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

jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 8c62fc5f6 [#5000] improvement(server): Move tag object API to object 
path (#5027)
8c62fc5f6 is described below

commit 8c62fc5f67ad74020e132fffb7c45c8fb204584e
Author: roryqi <[email protected]>
AuthorDate: Fri Sep 27 09:54:14 2024 +0800

    [#5000] improvement(server): Move tag object API to object path (#5027)
    
    ### What changes were proposed in this pull request?
    
    Move tag related about metadata object API to object path
    
    ### Why are the changes needed?
    
    Fix: #5000
    
    ### Does this PR introduce _any_ user-facing change?
    Modified the document
    
    ### How was this patch tested?
    Modified the UT.
---
 .../client/MetadataObjectTagOperations.java        |   2 +-
 .../apache/gravitino/client/TestSupportTags.java   |  19 +-
 docs/manage-tags-in-gravitino.md                   |  22 +-
 docs/open-api/openapi.yaml                         |   8 +-
 docs/open-api/tags.yaml                            |   4 +-
 ...tions.java => MetadataObjectTagOperations.java} | 296 ++--------
 .../gravitino/server/web/rest/TagOperations.java   | 230 ++------
 .../web/rest/TestMetadataObjectTagOperations.java  | 639 +++++++++++++++++++++
 8 files changed, 763 insertions(+), 457 deletions(-)

diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectTagOperations.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectTagOperations.java
index 1aba1c888..2ac84bdbf 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectTagOperations.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectTagOperations.java
@@ -52,7 +52,7 @@ class MetadataObjectTagOperations implements SupportsTags {
     this.restClient = restClient;
     this.tagRequestPath =
         String.format(
-            "api/metalakes/%s/tags/%s/%s",
+            "api/metalakes/%s/objects/%s/%s/tags",
             metalakeName,
             metadataObject.type().name().toLowerCase(Locale.ROOT),
             metadataObject.fullName());
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
index 095d78549..a80fb3246 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
@@ -343,10 +343,11 @@ public class TestSupportTags extends TestBase {
     String path =
         "/api/metalakes/"
             + METALAKE_NAME
-            + "/tags/"
+            + "/objects/"
             + metadataObject.type().name().toLowerCase(Locale.ROOT)
             + "/"
-            + metadataObject.fullName();
+            + metadataObject.fullName()
+            + "/tags";
 
     String[] tags = new String[] {"tag1", "tag2"};
     NameListResponse resp = new NameListResponse(tags);
@@ -383,10 +384,11 @@ public class TestSupportTags extends TestBase {
     String path =
         "/api/metalakes/"
             + METALAKE_NAME
-            + "/tags/"
+            + "/objects/"
             + metadataObject.type().name().toLowerCase(Locale.ROOT)
             + "/"
-            + metadataObject.fullName();
+            + metadataObject.fullName()
+            + "/tags";
 
     TagDTO tag1 =
         TagDTO.builder()
@@ -435,11 +437,11 @@ public class TestSupportTags extends TestBase {
     String path =
         "/api/metalakes/"
             + METALAKE_NAME
-            + "/tags/"
+            + "/objects/"
             + metadataObject.type().name().toLowerCase(Locale.ROOT)
             + "/"
             + metadataObject.fullName()
-            + "/tag1";
+            + "/tags/tag1";
 
     TagDTO tag1 =
         TagDTO.builder()
@@ -476,10 +478,11 @@ public class TestSupportTags extends TestBase {
     String path =
         "/api/metalakes/"
             + METALAKE_NAME
-            + "/tags/"
+            + "/objects/"
             + metadataObject.type().name().toLowerCase(Locale.ROOT)
             + "/"
-            + metadataObject.fullName();
+            + metadataObject.fullName()
+            + "/tags";
 
     String[] tagsToAdd = new String[] {"tag1", "tag2"};
     String[] tagsToRemove = new String[] {"tag3", "tag4"};
diff --git a/docs/manage-tags-in-gravitino.md b/docs/manage-tags-in-gravitino.md
index a02bf4faf..ac088a7c2 100644
--- a/docs/manage-tags-in-gravitino.md
+++ b/docs/manage-tags-in-gravitino.md
@@ -212,7 +212,7 @@ Gravitino allows you to associate and disassociate tags 
with metadata objects. C
 You can associate and disassociate tags with a metadata object by providing 
the object type, object
 name and tag names.
 
-The request path for REST API is 
`/api/metalakes/{metalake}/tags/{metadataObjectType}/{metadataObjectName}`.
+The request path for REST API is 
`/api/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectName}/tags`.
 
 <Tabs groupId='language' queryString>
 <TabItem value="shell" label="Shell">
@@ -222,12 +222,12 @@ curl -X POST -H "Accept: 
application/vnd.gravitino.v1+json" \
 -H "Content-Type: application/json" -d '{
   "tagsToAdd": ["tag1", "tag2"],
   "tagsToRemove": ["tag3"]
-}' http://localhost:8090/api/metalakes/test/tags/catalog/catalog1
+}' http://localhost:8090/api/metalakes/test/objects/catalog/catalog1/tags
 
 curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
 -H "Content-Type: application/json" -d '{
   "tagsToAdd": ["tag1"]
-}' http://localhost:8090/api/metalakes/test/tags/schema/catalog1.schema1
+}' 
http://localhost:8090/api/metalakes/test/objects/schema/catalog1.schema1/tags
 ```
 
 </TabItem>
@@ -252,23 +252,23 @@ You can list all the tags associated with a metadata 
object. The tags in Graviti
 inheritable, so listing tags of a metadata object will also list the tags of 
its parent metadata
 objects.
 
-The request path for REST API is 
`/api/metalakes/{metalake}/tags/{metadataObjectType}/{metadataObjectName}`.
+The request path for REST API is 
`/api/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectName}/tags`.
 
 <Tabs groupId='language' queryString>
 <TabItem value="shell" label="Shell">
 
 ```shell
 curl -X GET -H "Accept: application/vnd.gravitino.v1+json" \
-http://localhost:8090/api/metalakes/test/tags/catalog/catalog1
+http://localhost:8090/api/metalakes/test/objects/catalog/catalog1/tags
 
 curl -X GET -H "Accept: application/vnd.gravitino.v1+json" \
-http://localhost:8090/api/metalakes/test/tags/schema/catalog1.schema1
+http://localhost:8090/api/metalakes/test/objects/schema/catalog1.schema1/tags
 
 curl -X GET -H "Accept: application/vnd.gravitino.v1+json" \
-http://localhost:8090/api/metalakes/test/tags/catalog/catalog1?details=true
+http://localhost:8090/api/metalakes/test/objects/catalog/catalog1/tags?details=true
 
 curl -X GET -H "Accept: application/vnd.gravitino.v1+json" \
-http://localhost:8090/api/metalakes/test/tags/schema/catalog1.schema1?details=true
+http://localhost:8090/api/metalakes/test/objects/schema/catalog1.schema1/tags?details=true
 ```
 
 </TabItem>
@@ -291,17 +291,17 @@ Tag[] tagsInfo = schema1.supportsTags().listTagsInfo();
 
 You can get an associated tag by its name for a metadata object.
 
-The request path for REST API is 
`/api/metalakes/{metalake}/tags/{metadataObjectType}/{metadataObjectName}/{tagName}`.
+The request path for REST API is 
`/api/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectName}/tags/{tagName}`.
 
 <Tabs groupId='language' queryString>
 <TabItem value="shell" label="Shell">
 
 ```shell
 curl -X GET -H "Accept: application/vnd.gravitino.v1+json" \
-http://localhost:8090/api/metalakes/test/tags/catalog/catalog1/tag1
+http://localhost:8090/api/metalakes/test/objects/catalog/catalog1/tags/tag1
 
 curl -X GET -H "Accept: application/vnd.gravitino.v1+json" \
-http://localhost:8090/api/metalakes/test/tags/schema/catalog1.schema1/tag1
+http://localhost:8090/api/metalakes/test/objects/schema/catalog1.schema1/tags/tag1
 ```
 
 </TabItem>
diff --git a/docs/open-api/openapi.yaml b/docs/open-api/openapi.yaml
index 2305eafce..2c8ab1bfe 100644
--- a/docs/open-api/openapi.yaml
+++ b/docs/open-api/openapi.yaml
@@ -62,11 +62,11 @@ paths:
   /metalakes/{metalake}/tags/{tag}:
     $ref: "./tags.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1tags~1%7Btag%7D"
 
-  /metalakes/{metalake}/tags/{metadataObjectType}/{metadataObjectFullName}:
-    $ref: 
"./tags.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1tags~1%7BmetadataObjectType%7D~1%7BmetadataObjectFullName%7D"
+  
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/tags:
+    $ref: 
"./tags.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1objects~1%7BmetadataObjectType%7D~1%7BmetadataObjectFullName%7D~1tags"
 
-  
/metalakes/{metalake}/tags/{metadataObjectType}/{metadataObjectFullName}/{tag}:
-    $ref: 
"./tags.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1tags~1%7BmetadataObjectType%7D~1%7BmetadataObjectFullName%7D~1%7Btag%7D"
+  
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/tags/{tag}:
+    $ref: 
"./tags.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1objects~1%7BmetadataObjectType%7D~1%7BmetadataObjectFullName%7D~1tags~1%7Btag%7D"
 
   /metalakes/{metalake}/tags/{tag}/objects:
     $ref: 
"./tags.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1tags~1%7Btag%7D~1objects"
diff --git a/docs/open-api/tags.yaml b/docs/open-api/tags.yaml
index 7264c0673..9419b8f6e 100644
--- a/docs/open-api/tags.yaml
+++ b/docs/open-api/tags.yaml
@@ -178,7 +178,7 @@ paths:
           $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
 
 
-  /metalakes/{metalake}/tags/{metadataObjectType}/{metadataObjectFullName}:
+  
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/tags:
     parameters:
       - $ref: "./openapi.yaml#/components/parameters/metalake"
       - $ref: "./openapi.yaml#/components/parameters/metadataObjectType"
@@ -245,7 +245,7 @@ paths:
           $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
 
 
-  
/metalakes/{metalake}/tags/{metadataObjectType}/{metadataObjectFullName}/{tag}:
+  
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/tags/{tag}:
     parameters:
       - $ref: "./openapi.yaml#/components/parameters/metalake"
       - $ref: "./openapi.yaml#/components/parameters/metadataObjectType"
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
similarity index 56%
copy from 
server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
copy to 
server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
index 733623977..c8668d225 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
@@ -28,11 +28,9 @@ import java.util.Locale;
 import java.util.Optional;
 import javax.inject.Inject;
 import javax.servlet.http.HttpServletRequest;
-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;
@@ -42,188 +40,106 @@ import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.MetadataObjects;
-import org.apache.gravitino.dto.requests.TagCreateRequest;
-import org.apache.gravitino.dto.requests.TagUpdateRequest;
-import org.apache.gravitino.dto.requests.TagUpdatesRequest;
 import org.apache.gravitino.dto.requests.TagsAssociateRequest;
-import org.apache.gravitino.dto.responses.DropResponse;
-import org.apache.gravitino.dto.responses.MetadataObjectListResponse;
 import org.apache.gravitino.dto.responses.NameListResponse;
 import org.apache.gravitino.dto.responses.TagListResponse;
 import org.apache.gravitino.dto.responses.TagResponse;
-import org.apache.gravitino.dto.tag.MetadataObjectDTO;
 import org.apache.gravitino.dto.tag.TagDTO;
 import org.apache.gravitino.dto.util.DTOConverters;
 import org.apache.gravitino.exceptions.NoSuchTagException;
 import org.apache.gravitino.metrics.MetricNames;
 import org.apache.gravitino.server.web.Utils;
 import org.apache.gravitino.tag.Tag;
-import org.apache.gravitino.tag.TagChange;
 import org.apache.gravitino.tag.TagManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-@Path("metalakes/{metalake}/tags")
-public class TagOperations {
-
-  private static final Logger LOG = 
LoggerFactory.getLogger(TagOperations.class);
+@Path("/metalakes/{metalake}/objects/{type}/{fullName}/tags")
+public class MetadataObjectTagOperations {
+  private static final Logger LOG = 
LoggerFactory.getLogger(MetadataObjectTagOperations.class);
 
   private final TagManager tagManager;
 
   @Context private HttpServletRequest httpRequest;
 
   @Inject
-  public TagOperations(TagManager tagManager) {
+  public MetadataObjectTagOperations(TagManager tagManager) {
     this.tagManager = tagManager;
   }
 
-  @GET
-  @Produces("application/vnd.gravitino.v1+json")
-  @Timed(name = "list-tags." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
-  @ResponseMetered(name = "list-tags", absolute = true)
-  public Response listTags(
-      @PathParam("metalake") String metalake,
-      @QueryParam("details") @DefaultValue("false") boolean verbose) {
-    LOG.info(
-        "Received list tag {} request for metalake: {}", verbose ? "infos" : 
"names", metalake);
-
-    try {
-      return Utils.doAs(
-          httpRequest,
-          () -> {
-            if (verbose) {
-              Tag[] tags = tagManager.listTagsInfo(metalake);
-              TagDTO[] tagDTOs;
-              if (ArrayUtils.isEmpty(tags)) {
-                tagDTOs = new TagDTO[0];
-              } else {
-                tagDTOs =
-                    Arrays.stream(tags)
-                        .map(t -> DTOConverters.toDTO(t, Optional.empty()))
-                        .toArray(TagDTO[]::new);
-              }
-
-              LOG.info("List {} tags info under metalake: {}", tagDTOs.length, 
metalake);
-              return Utils.ok(new TagListResponse(tagDTOs));
-
-            } else {
-              String[] tagNames = tagManager.listTags(metalake);
-              tagNames = tagNames == null ? new String[0] : tagNames;
-
-              LOG.info("List {} tags under metalake: {}", tagNames.length, 
metalake);
-              return Utils.ok(new NameListResponse(tagNames));
-            }
-          });
-    } catch (Exception e) {
-      return ExceptionHandlers.handleTagException(OperationType.LIST, "", 
metalake, e);
-    }
-  }
-
-  @POST
-  @Produces("application/vnd.gravitino.v1+json")
-  @Timed(name = "create-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
-  @ResponseMetered(name = "create-tag", absolute = true)
-  public Response createTag(@PathParam("metalake") String metalake, 
TagCreateRequest request) {
-    LOG.info("Received create tag request under metalake: {}", metalake);
-
-    try {
-      return Utils.doAs(
-          httpRequest,
-          () -> {
-            request.validate();
-            Tag tag =
-                tagManager.createTag(
-                    metalake, request.getName(), request.getComment(), 
request.getProperties());
-
-            LOG.info("Created tag: {} under metalake: {}", tag.name(), 
metalake);
-            return Utils.ok(new TagResponse(DTOConverters.toDTO(tag, 
Optional.empty())));
-          });
-    } catch (Exception e) {
-      return ExceptionHandlers.handleTagException(
-          OperationType.CREATE, request.getName(), metalake, e);
-    }
+  // TagOperations will reuse this class to be compatible with legacy 
interfaces.
+  void setHttpRequest(HttpServletRequest httpRequest) {
+    this.httpRequest = httpRequest;
   }
 
   @GET
   @Path("{tag}")
   @Produces("application/vnd.gravitino.v1+json")
-  @Timed(name = "get-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
-  @ResponseMetered(name = "get-tag", absolute = true)
-  public Response getTag(@PathParam("metalake") String metalake, 
@PathParam("tag") String name) {
-    LOG.info("Received get tag request for tag: {} under metalake: {}", name, 
metalake);
-
-    try {
-      return Utils.doAs(
-          httpRequest,
-          () -> {
-            Tag tag = tagManager.getTag(metalake, name);
-            LOG.info("Get tag: {} under metalake: {}", name, metalake);
-            return Utils.ok(new TagResponse(DTOConverters.toDTO(tag, 
Optional.empty())));
-          });
-    } catch (Exception e) {
-      return ExceptionHandlers.handleTagException(OperationType.GET, name, 
metalake, e);
-    }
-  }
-
-  @PUT
-  @Path("{tag}")
-  @Produces("application/vnd.gravitino.v1+json")
-  @Timed(name = "alter-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
-  @ResponseMetered(name = "alter-tag", absolute = true)
-  public Response alterTag(
+  @Timed(name = "get-object-tag." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "get-object-tag", absolute = true)
+  public Response getTagForObject(
       @PathParam("metalake") String metalake,
-      @PathParam("tag") String name,
-      TagUpdatesRequest request) {
-    LOG.info("Received alter tag request for tag: {} under metalake: {}", 
name, metalake);
+      @PathParam("type") String type,
+      @PathParam("fullName") String fullName,
+      @PathParam("tag") String tagName) {
+    LOG.info(
+        "Received get tag {} request for object type: {}, full name: {} under 
metalake: {}",
+        tagName,
+        type,
+        fullName,
+        metalake);
 
     try {
       return Utils.doAs(
           httpRequest,
           () -> {
-            request.validate();
-
-            TagChange[] changes =
-                request.getUpdates().stream()
-                    .map(TagUpdateRequest::tagChange)
-                    .toArray(TagChange[]::new);
-            Tag tag = tagManager.alterTag(metalake, name, changes);
-
-            LOG.info("Altered tag: {} under metalake: {}", name, metalake);
-            return Utils.ok(new TagResponse(DTOConverters.toDTO(tag, 
Optional.empty())));
-          });
-    } catch (Exception e) {
-      return ExceptionHandlers.handleTagException(OperationType.ALTER, name, 
metalake, e);
-    }
-  }
+            MetadataObject object =
+                MetadataObjects.parse(
+                    fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+            Optional<Tag> tag = getTagForObject(metalake, object, tagName);
+            Optional<TagDTO> tagDTO = tag.map(t -> DTOConverters.toDTO(t, 
Optional.of(false)));
 
-  @DELETE
-  @Path("{tag}")
-  @Produces("application/vnd.gravitino.v1+json")
-  @Timed(name = "delete-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
-  @ResponseMetered(name = "delete-tag", absolute = true)
-  public Response deleteTag(@PathParam("metalake") String metalake, 
@PathParam("tag") String name) {
-    LOG.info("Received delete tag request for tag: {} under metalake: {}", 
name, metalake);
+            MetadataObject parentObject = MetadataObjects.parent(object);
+            while (!tag.isPresent() && parentObject != null) {
+              tag = getTagForObject(metalake, parentObject, tagName);
+              tagDTO = tag.map(t -> DTOConverters.toDTO(t, Optional.of(true)));
+              parentObject = MetadataObjects.parent(parentObject);
+            }
 
-    try {
-      return Utils.doAs(
-          httpRequest,
-          () -> {
-            boolean deleted = tagManager.deleteTag(metalake, name);
-            if (!deleted) {
-              LOG.warn("Failed to delete tag {} under metalake {}", name, 
metalake);
+            if (!tagDTO.isPresent()) {
+              LOG.warn(
+                  "Tag {} not found for object type: {}, full name: {} under 
metalake: {}",
+                  tagName,
+                  type,
+                  fullName,
+                  metalake);
+              return Utils.notFound(
+                  NoSuchTagException.class.getSimpleName(),
+                  "Tag not found: "
+                      + tagName
+                      + " for object type: "
+                      + type
+                      + ", full name: "
+                      + fullName
+                      + " under metalake: "
+                      + metalake);
             } else {
-              LOG.info("Deleted tag: {} under metalake: {}", name, metalake);
+              LOG.info(
+                  "Get tag: {} for object type: {}, full name: {} under 
metalake: {}",
+                  tagName,
+                  type,
+                  fullName,
+                  metalake);
+              return Utils.ok(new TagResponse(tagDTO.get()));
             }
-
-            return Utils.ok(new DropResponse(deleted));
           });
+
     } catch (Exception e) {
-      return ExceptionHandlers.handleTagException(OperationType.DELETE, name, 
metalake, e);
+      return ExceptionHandlers.handleTagException(OperationType.GET, tagName, 
fullName, e);
     }
   }
 
   @GET
-  @Path("{type}/{fullName}")
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "list-object-tags." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "list-object-tags", absolute = true)
@@ -300,107 +216,7 @@ public class TagOperations {
     }
   }
 
-  @GET
-  @Path("{type}/{fullName}/{tag}")
-  @Produces("application/vnd.gravitino.v1+json")
-  @Timed(name = "get-object-tag." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
-  @ResponseMetered(name = "get-object-tag", absolute = true)
-  public Response getTagForObject(
-      @PathParam("metalake") String metalake,
-      @PathParam("type") String type,
-      @PathParam("fullName") String fullName,
-      @PathParam("tag") String tagName) {
-    LOG.info(
-        "Received get tag {} request for object type: {}, full name: {} under 
metalake: {}",
-        tagName,
-        type,
-        fullName,
-        metalake);
-
-    try {
-      return Utils.doAs(
-          httpRequest,
-          () -> {
-            MetadataObject object =
-                MetadataObjects.parse(
-                    fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
-            Optional<Tag> tag = getTagForObject(metalake, object, tagName);
-            Optional<TagDTO> tagDTO = tag.map(t -> DTOConverters.toDTO(t, 
Optional.of(false)));
-
-            MetadataObject parentObject = MetadataObjects.parent(object);
-            while (!tag.isPresent() && parentObject != null) {
-              tag = getTagForObject(metalake, parentObject, tagName);
-              tagDTO = tag.map(t -> DTOConverters.toDTO(t, Optional.of(true)));
-              parentObject = MetadataObjects.parent(parentObject);
-            }
-
-            if (!tagDTO.isPresent()) {
-              LOG.warn(
-                  "Tag {} not found for object type: {}, full name: {} under 
metalake: {}",
-                  tagName,
-                  type,
-                  fullName,
-                  metalake);
-              return Utils.notFound(
-                  NoSuchTagException.class.getSimpleName(),
-                  "Tag not found: "
-                      + tagName
-                      + " for object type: "
-                      + type
-                      + ", full name: "
-                      + fullName
-                      + " under metalake: "
-                      + metalake);
-            } else {
-              LOG.info(
-                  "Get tag: {} for object type: {}, full name: {} under 
metalake: {}",
-                  tagName,
-                  type,
-                  fullName,
-                  metalake);
-              return Utils.ok(new TagResponse(tagDTO.get()));
-            }
-          });
-
-    } catch (Exception e) {
-      return ExceptionHandlers.handleTagException(OperationType.GET, tagName, 
fullName, e);
-    }
-  }
-
-  @GET
-  @Path("{tag}/objects")
-  @Produces("application/vnd.gravitino.v1+json")
-  @Timed(name = "list-objects-for-tag." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
-  @ResponseMetered(name = "list-objects-for-tag", absolute = true)
-  public Response listMetadataObjectsForTag(
-      @PathParam("metalake") String metalake, @PathParam("tag") String 
tagName) {
-    LOG.info("Received list objects for tag: {} under metalake: {}", tagName, 
metalake);
-
-    try {
-      return Utils.doAs(
-          httpRequest,
-          () -> {
-            MetadataObject[] objects = 
tagManager.listMetadataObjectsForTag(metalake, tagName);
-            objects = objects == null ? new MetadataObject[0] : objects;
-
-            LOG.info(
-                "List {} objects for tag: {} under metalake: {}",
-                objects.length,
-                tagName,
-                metalake);
-
-            MetadataObjectDTO[] objectDTOs =
-                
Arrays.stream(objects).map(DTOConverters::toDTO).toArray(MetadataObjectDTO[]::new);
-            return Utils.ok(new MetadataObjectListResponse(objectDTOs));
-          });
-
-    } catch (Exception e) {
-      return ExceptionHandlers.handleTagException(OperationType.LIST, "", 
tagName, e);
-    }
-  }
-
   @POST
-  @Path("{type}/{fullName}")
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "associate-object-tags." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "associate-object-tags", absolute = true)
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java 
b/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
index 733623977..7fdd2fc69 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
@@ -20,11 +20,7 @@ package org.apache.gravitino.server.web.rest;
 
 import com.codahale.metrics.annotation.ResponseMetered;
 import com.codahale.metrics.annotation.Timed;
-import com.google.common.collect.Lists;
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
 import java.util.Optional;
 import javax.inject.Inject;
 import javax.servlet.http.HttpServletRequest;
@@ -41,7 +37,6 @@ import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.gravitino.MetadataObject;
-import org.apache.gravitino.MetadataObjects;
 import org.apache.gravitino.dto.requests.TagCreateRequest;
 import org.apache.gravitino.dto.requests.TagUpdateRequest;
 import org.apache.gravitino.dto.requests.TagUpdatesRequest;
@@ -54,7 +49,6 @@ import org.apache.gravitino.dto.responses.TagResponse;
 import org.apache.gravitino.dto.tag.MetadataObjectDTO;
 import org.apache.gravitino.dto.tag.TagDTO;
 import org.apache.gravitino.dto.util.DTOConverters;
-import org.apache.gravitino.exceptions.NoSuchTagException;
 import org.apache.gravitino.metrics.MetricNames;
 import org.apache.gravitino.server.web.Utils;
 import org.apache.gravitino.tag.Tag;
@@ -222,151 +216,6 @@ public class TagOperations {
     }
   }
 
-  @GET
-  @Path("{type}/{fullName}")
-  @Produces("application/vnd.gravitino.v1+json")
-  @Timed(name = "list-object-tags." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
-  @ResponseMetered(name = "list-object-tags", absolute = true)
-  public Response listTagsForMetadataObject(
-      @PathParam("metalake") String metalake,
-      @PathParam("type") String type,
-      @PathParam("fullName") String fullName,
-      @QueryParam("details") @DefaultValue("false") boolean verbose) {
-    LOG.info(
-        "Received list tag {} request for object type: {}, full name: {} under 
metalake: {}",
-        verbose ? "infos" : "names",
-        type,
-        fullName,
-        metalake);
-
-    try {
-      return Utils.doAs(
-          httpRequest,
-          () -> {
-            MetadataObject object =
-                MetadataObjects.parse(
-                    fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
-
-            List<TagDTO> tags = Lists.newArrayList();
-            Tag[] nonInheritedTags = 
tagManager.listTagsInfoForMetadataObject(metalake, object);
-            if (ArrayUtils.isNotEmpty(nonInheritedTags)) {
-              Collections.addAll(
-                  tags,
-                  Arrays.stream(nonInheritedTags)
-                      .map(t -> DTOConverters.toDTO(t, Optional.of(false)))
-                      .toArray(TagDTO[]::new));
-            }
-
-            MetadataObject parentObject = MetadataObjects.parent(object);
-            while (parentObject != null) {
-              Tag[] inheritedTags =
-                  tagManager.listTagsInfoForMetadataObject(metalake, 
parentObject);
-              if (ArrayUtils.isNotEmpty(inheritedTags)) {
-                Collections.addAll(
-                    tags,
-                    Arrays.stream(inheritedTags)
-                        .map(t -> DTOConverters.toDTO(t, Optional.of(true)))
-                        .toArray(TagDTO[]::new));
-              }
-              parentObject = MetadataObjects.parent(parentObject);
-            }
-
-            if (verbose) {
-              LOG.info(
-                  "List {} tags info for object type: {}, full name: {} under 
metalake: {}",
-                  tags.size(),
-                  type,
-                  fullName,
-                  metalake);
-              return Utils.ok(new TagListResponse(tags.toArray(new 
TagDTO[0])));
-
-            } else {
-              // Due to same name tag will be associated to both parent and 
child objects, so we
-              // need to deduplicate the tag names.
-              String[] tagNames = 
tags.stream().map(TagDTO::name).distinct().toArray(String[]::new);
-
-              LOG.info(
-                  "List {} tags for object type: {}, full name: {} under 
metalake: {}",
-                  tagNames.length,
-                  type,
-                  fullName,
-                  metalake);
-              return Utils.ok(new NameListResponse(tagNames));
-            }
-          });
-
-    } catch (Exception e) {
-      return ExceptionHandlers.handleTagException(OperationType.LIST, "", 
fullName, e);
-    }
-  }
-
-  @GET
-  @Path("{type}/{fullName}/{tag}")
-  @Produces("application/vnd.gravitino.v1+json")
-  @Timed(name = "get-object-tag." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
-  @ResponseMetered(name = "get-object-tag", absolute = true)
-  public Response getTagForObject(
-      @PathParam("metalake") String metalake,
-      @PathParam("type") String type,
-      @PathParam("fullName") String fullName,
-      @PathParam("tag") String tagName) {
-    LOG.info(
-        "Received get tag {} request for object type: {}, full name: {} under 
metalake: {}",
-        tagName,
-        type,
-        fullName,
-        metalake);
-
-    try {
-      return Utils.doAs(
-          httpRequest,
-          () -> {
-            MetadataObject object =
-                MetadataObjects.parse(
-                    fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
-            Optional<Tag> tag = getTagForObject(metalake, object, tagName);
-            Optional<TagDTO> tagDTO = tag.map(t -> DTOConverters.toDTO(t, 
Optional.of(false)));
-
-            MetadataObject parentObject = MetadataObjects.parent(object);
-            while (!tag.isPresent() && parentObject != null) {
-              tag = getTagForObject(metalake, parentObject, tagName);
-              tagDTO = tag.map(t -> DTOConverters.toDTO(t, Optional.of(true)));
-              parentObject = MetadataObjects.parent(parentObject);
-            }
-
-            if (!tagDTO.isPresent()) {
-              LOG.warn(
-                  "Tag {} not found for object type: {}, full name: {} under 
metalake: {}",
-                  tagName,
-                  type,
-                  fullName,
-                  metalake);
-              return Utils.notFound(
-                  NoSuchTagException.class.getSimpleName(),
-                  "Tag not found: "
-                      + tagName
-                      + " for object type: "
-                      + type
-                      + ", full name: "
-                      + fullName
-                      + " under metalake: "
-                      + metalake);
-            } else {
-              LOG.info(
-                  "Get tag: {} for object type: {}, full name: {} under 
metalake: {}",
-                  tagName,
-                  type,
-                  fullName,
-                  metalake);
-              return Utils.ok(new TagResponse(tagDTO.get()));
-            }
-          });
-
-    } catch (Exception e) {
-      return ExceptionHandlers.handleTagException(OperationType.GET, tagName, 
fullName, e);
-    }
-  }
-
   @GET
   @Path("{tag}/objects")
   @Produces("application/vnd.gravitino.v1+json")
@@ -399,6 +248,41 @@ public class TagOperations {
     }
   }
 
+  @Deprecated
+  @GET
+  @Path("{type}/{fullName}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "list-object-tags." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "list-object-tags", absolute = true)
+  public Response listTagsForMetadataObject(
+      @PathParam("metalake") String metalake,
+      @PathParam("type") String type,
+      @PathParam("fullName") String fullName,
+      @QueryParam("details") @DefaultValue("false") boolean verbose) {
+    MetadataObjectTagOperations metadataObjectTagOperations =
+        new MetadataObjectTagOperations(tagManager);
+    metadataObjectTagOperations.setHttpRequest(httpRequest);
+    return metadataObjectTagOperations.listTagsForMetadataObject(metalake, 
type, fullName, verbose);
+  }
+
+  @Deprecated
+  @GET
+  @Path("{type}/{fullName}/{tag}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "get-object-tag." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "get-object-tag", absolute = true)
+  public Response getTagForObject(
+      @PathParam("metalake") String metalake,
+      @PathParam("type") String type,
+      @PathParam("fullName") String fullName,
+      @PathParam("tag") String tagName) {
+    MetadataObjectTagOperations metadataObjectTagOperations =
+        new MetadataObjectTagOperations(tagManager);
+    metadataObjectTagOperations.setHttpRequest(httpRequest);
+    return metadataObjectTagOperations.getTagForObject(metalake, type, 
fullName, tagName);
+  }
+
+  @Deprecated
   @POST
   @Path("{type}/{fullName}")
   @Produces("application/vnd.gravitino.v1+json")
@@ -409,45 +293,9 @@ public class TagOperations {
       @PathParam("type") String type,
       @PathParam("fullName") String fullName,
       TagsAssociateRequest request) {
-    LOG.info(
-        "Received associate tags request for object type: {}, full name: {} 
under metalake: {}",
-        type,
-        fullName,
-        metalake);
-
-    try {
-      return Utils.doAs(
-          httpRequest,
-          () -> {
-            request.validate();
-            MetadataObject object =
-                MetadataObjects.parse(
-                    fullName, 
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
-            String[] tagNames =
-                tagManager.associateTagsForMetadataObject(
-                    metalake, object, request.getTagsToAdd(), 
request.getTagsToRemove());
-            tagNames = tagNames == null ? new String[0] : tagNames;
-
-            LOG.info(
-                "Associated tags: {} for object type: {}, full name: {} under 
metalake: {}",
-                Arrays.toString(tagNames),
-                type,
-                fullName,
-                metalake);
-            return Utils.ok(new NameListResponse(tagNames));
-          });
-
-    } catch (Exception e) {
-      return ExceptionHandlers.handleTagException(OperationType.ASSOCIATE, "", 
fullName, e);
-    }
-  }
-
-  private Optional<Tag> getTagForObject(String metalake, MetadataObject 
object, String tagName) {
-    try {
-      return Optional.ofNullable(tagManager.getTagForMetadataObject(metalake, 
object, tagName));
-    } catch (NoSuchTagException e) {
-      LOG.info("Tag {} not found for object: {}", tagName, object);
-      return Optional.empty();
-    }
+    MetadataObjectTagOperations metadataObjectTagOperations =
+        new MetadataObjectTagOperations(tagManager);
+    metadataObjectTagOperations.setHttpRequest(httpRequest);
+    return metadataObjectTagOperations.associateTagsForObject(metalake, type, 
fullName, request);
   }
 }
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
new file mode 100644
index 000000000..8e0324bea
--- /dev/null
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
@@ -0,0 +1,639 @@
+/*
+ * 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.gravitino.server.web.rest;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.dto.requests.TagsAssociateRequest;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.dto.responses.NameListResponse;
+import org.apache.gravitino.dto.responses.TagListResponse;
+import org.apache.gravitino.dto.responses.TagResponse;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.exceptions.TagAlreadyAssociatedException;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.TagEntity;
+import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagManager;
+import org.glassfish.jersey.internal.inject.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestMetadataObjectTagOperations extends JerseyTest {
+
+  private static class MockServletRequestFactory extends 
ServletRequestFactoryBase {
+
+    @Override
+    public HttpServletRequest get() {
+      HttpServletRequest request = mock(HttpServletRequest.class);
+      when(request.getRemoteUser()).thenReturn(null);
+      return request;
+    }
+  }
+
+  private TagManager tagManager = mock(TagManager.class);
+
+  private String metalake = "test_metalake";
+
+  private AuditInfo testAuditInfo1 =
+      
AuditInfo.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+
+  @Override
+  protected Application configure() {
+    try {
+      forceSet(
+          TestProperties.CONTAINER_PORT, 
String.valueOf(RESTUtils.findAvailablePort(2000, 3000)));
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    ResourceConfig resourceConfig = new ResourceConfig();
+    resourceConfig.register(MetadataObjectTagOperations.class);
+    resourceConfig.register(
+        new AbstractBinder() {
+          @Override
+          protected void configure() {
+            bind(tagManager).to(TagManager.class).ranked(2);
+            
bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class);
+          }
+        });
+
+    return resourceConfig;
+  }
+
+  @Test
+  public void testListTagsForObject() {
+    MetadataObject catalog = MetadataObjects.parse("object1", 
MetadataObject.Type.CATALOG);
+    MetadataObject schema = MetadataObjects.parse("object1.object2", 
MetadataObject.Type.SCHEMA);
+    MetadataObject table =
+        MetadataObjects.parse("object1.object2.object3", 
MetadataObject.Type.TABLE);
+    MetadataObject column =
+        MetadataObjects.parse("object1.object2.object3.object4", 
MetadataObject.Type.COLUMN);
+
+    Tag[] catalogTagInfos =
+        new Tag[] {
+          
TagEntity.builder().withName("tag1").withId(1L).withAuditInfo(testAuditInfo1).build()
+        };
+    when(tagManager.listTagsInfoForMetadataObject(metalake, 
catalog)).thenReturn(catalogTagInfos);
+
+    Tag[] schemaTagInfos =
+        new Tag[] {
+          
TagEntity.builder().withName("tag3").withId(1L).withAuditInfo(testAuditInfo1).build()
+        };
+    when(tagManager.listTagsInfoForMetadataObject(metalake, 
schema)).thenReturn(schemaTagInfos);
+
+    Tag[] tableTagInfos =
+        new Tag[] {
+          
TagEntity.builder().withName("tag5").withId(1L).withAuditInfo(testAuditInfo1).build()
+        };
+    when(tagManager.listTagsInfoForMetadataObject(metalake, 
table)).thenReturn(tableTagInfos);
+
+    Tag[] columnTagInfos =
+        new Tag[] {
+          
TagEntity.builder().withName("tag7").withId(1L).withAuditInfo(testAuditInfo1).build()
+        };
+    when(tagManager.listTagsInfoForMetadataObject(metalake, 
column)).thenReturn(columnTagInfos);
+
+    // Test catalog tags
+    Response response =
+        target(basePath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("/tags")
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+
+    TagListResponse tagListResponse = 
response.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResponse.getCode());
+    Assertions.assertEquals(catalogTagInfos.length, 
tagListResponse.getTags().length);
+
+    Map<String, Tag> resultTags =
+        Arrays.stream(tagListResponse.getTags())
+            .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+    Assertions.assertTrue(resultTags.containsKey("tag1"));
+    Assertions.assertFalse(resultTags.get("tag1").inherited().get());
+
+    Response response1 =
+        target(basePath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("tags")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response1.getStatus());
+
+    NameListResponse nameListResponse = 
response1.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse.getCode());
+    Assertions.assertEquals(catalogTagInfos.length, 
nameListResponse.getNames().length);
+    Assertions.assertArrayEquals(
+        Arrays.stream(catalogTagInfos).map(Tag::name).toArray(String[]::new),
+        nameListResponse.getNames());
+
+    // Test schema tags
+    Response response2 =
+        target(basePath(metalake))
+            .path(schema.type().toString())
+            .path(schema.fullName())
+            .path("tags")
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response2.getStatus());
+
+    TagListResponse tagListResponse1 = 
response2.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResponse1.getCode());
+    Assertions.assertEquals(
+        schemaTagInfos.length + catalogTagInfos.length, 
tagListResponse1.getTags().length);
+
+    Map<String, Tag> resultTags1 =
+        Arrays.stream(tagListResponse1.getTags())
+            .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+    Assertions.assertTrue(resultTags1.containsKey("tag1"));
+    Assertions.assertTrue(resultTags1.containsKey("tag3"));
+
+    Assertions.assertTrue(resultTags1.get("tag1").inherited().get());
+    Assertions.assertFalse(resultTags1.get("tag3").inherited().get());
+
+    Response response3 =
+        target(basePath(metalake))
+            .path(schema.type().toString())
+            .path(schema.fullName())
+            .path("tags")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response3.getStatus());
+
+    NameListResponse nameListResponse1 = 
response3.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse1.getCode());
+    Assertions.assertEquals(
+        schemaTagInfos.length + catalogTagInfos.length, 
nameListResponse1.getNames().length);
+    Set<String> resultNames = Sets.newHashSet(nameListResponse1.getNames());
+    Assertions.assertTrue(resultNames.contains("tag1"));
+    Assertions.assertTrue(resultNames.contains("tag3"));
+
+    // Test table tags
+    Response response4 =
+        target(basePath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("tags")
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response4.getStatus());
+
+    TagListResponse tagListResponse2 = 
response4.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResponse2.getCode());
+    Assertions.assertEquals(
+        schemaTagInfos.length + catalogTagInfos.length + tableTagInfos.length,
+        tagListResponse2.getTags().length);
+
+    Map<String, Tag> resultTags2 =
+        Arrays.stream(tagListResponse2.getTags())
+            .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+    Assertions.assertTrue(resultTags2.containsKey("tag1"));
+    Assertions.assertTrue(resultTags2.containsKey("tag3"));
+    Assertions.assertTrue(resultTags2.containsKey("tag5"));
+
+    Assertions.assertTrue(resultTags2.get("tag1").inherited().get());
+    Assertions.assertTrue(resultTags2.get("tag3").inherited().get());
+    Assertions.assertFalse(resultTags2.get("tag5").inherited().get());
+
+    Response response5 =
+        target(basePath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("tags")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response5.getStatus());
+
+    NameListResponse nameListResponse2 = 
response5.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse2.getCode());
+    Assertions.assertEquals(
+        schemaTagInfos.length + catalogTagInfos.length + tableTagInfos.length,
+        nameListResponse2.getNames().length);
+
+    Set<String> resultNames1 = Sets.newHashSet(nameListResponse2.getNames());
+    Assertions.assertTrue(resultNames1.contains("tag1"));
+    Assertions.assertTrue(resultNames1.contains("tag3"));
+    Assertions.assertTrue(resultNames1.contains("tag5"));
+
+    // Test column tags
+    Response response6 =
+        target(basePath(metalake))
+            .path(column.type().toString())
+            .path(column.fullName())
+            .path("tags")
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response6.getStatus());
+
+    TagListResponse tagListResponse3 = 
response6.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResponse3.getCode());
+    Assertions.assertEquals(
+        schemaTagInfos.length
+            + catalogTagInfos.length
+            + tableTagInfos.length
+            + columnTagInfos.length,
+        tagListResponse3.getTags().length);
+
+    Map<String, Tag> resultTags3 =
+        Arrays.stream(tagListResponse3.getTags())
+            .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+    Assertions.assertTrue(resultTags3.containsKey("tag1"));
+    Assertions.assertTrue(resultTags3.containsKey("tag3"));
+    Assertions.assertTrue(resultTags3.containsKey("tag5"));
+    Assertions.assertTrue(resultTags3.containsKey("tag7"));
+
+    Assertions.assertTrue(resultTags3.get("tag1").inherited().get());
+    Assertions.assertTrue(resultTags3.get("tag3").inherited().get());
+    Assertions.assertTrue(resultTags3.get("tag5").inherited().get());
+    Assertions.assertFalse(resultTags3.get("tag7").inherited().get());
+
+    Response response7 =
+        target(basePath(metalake))
+            .path(column.type().toString())
+            .path(column.fullName())
+            .path("tags")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response7.getStatus());
+
+    NameListResponse nameListResponse3 = 
response7.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse3.getCode());
+
+    Assertions.assertEquals(
+        schemaTagInfos.length
+            + catalogTagInfos.length
+            + tableTagInfos.length
+            + columnTagInfos.length,
+        nameListResponse3.getNames().length);
+
+    Set<String> resultNames2 = Sets.newHashSet(nameListResponse3.getNames());
+    Assertions.assertTrue(resultNames2.contains("tag1"));
+    Assertions.assertTrue(resultNames2.contains("tag3"));
+    Assertions.assertTrue(resultNames2.contains("tag5"));
+    Assertions.assertTrue(resultNames2.contains("tag7"));
+  }
+
+  @Test
+  public void testGetTagForObject() {
+    TagEntity tag1 =
+        
TagEntity.builder().withName("tag1").withId(1L).withAuditInfo(testAuditInfo1).build();
+    MetadataObject catalog = MetadataObjects.parse("object1", 
MetadataObject.Type.CATALOG);
+    when(tagManager.getTagForMetadataObject(metalake, catalog, 
"tag1")).thenReturn(tag1);
+
+    TagEntity tag2 =
+        
TagEntity.builder().withName("tag2").withId(1L).withAuditInfo(testAuditInfo1).build();
+    MetadataObject schema = MetadataObjects.parse("object1.object2", 
MetadataObject.Type.SCHEMA);
+    when(tagManager.getTagForMetadataObject(metalake, schema, 
"tag2")).thenReturn(tag2);
+
+    TagEntity tag3 =
+        
TagEntity.builder().withName("tag3").withId(1L).withAuditInfo(testAuditInfo1).build();
+    MetadataObject table =
+        MetadataObjects.parse("object1.object2.object3", 
MetadataObject.Type.TABLE);
+    when(tagManager.getTagForMetadataObject(metalake, table, 
"tag3")).thenReturn(tag3);
+
+    TagEntity tag4 =
+        
TagEntity.builder().withName("tag4").withId(1L).withAuditInfo(testAuditInfo1).build();
+    MetadataObject column =
+        MetadataObjects.parse("object1.object2.object3.object4", 
MetadataObject.Type.COLUMN);
+    when(tagManager.getTagForMetadataObject(metalake, column, 
"tag4")).thenReturn(tag4);
+
+    // Test catalog tag
+    Response response =
+        target(basePath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("tags")
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+
+    TagResponse tagResponse = response.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse.getCode());
+
+    Tag respTag = tagResponse.getTag();
+    Assertions.assertEquals(tag1.name(), respTag.name());
+    Assertions.assertEquals(tag1.comment(), respTag.comment());
+    Assertions.assertFalse(respTag.inherited().get());
+
+    // Test schema tag
+    Response response1 =
+        target(basePath(metalake))
+            .path(schema.type().toString())
+            .path(schema.fullName())
+            .path("tags")
+            .path("tag2")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response1.getStatus());
+
+    TagResponse tagResponse1 = response1.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse1.getCode());
+
+    Tag respTag1 = tagResponse1.getTag();
+    Assertions.assertEquals(tag2.name(), respTag1.name());
+    Assertions.assertEquals(tag2.comment(), respTag1.comment());
+    Assertions.assertFalse(respTag1.inherited().get());
+
+    // Test table tag
+    Response response2 =
+        target(basePath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("tags")
+            .path("tag3")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response2.getStatus());
+
+    TagResponse tagResponse2 = response2.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse2.getCode());
+
+    Tag respTag2 = tagResponse2.getTag();
+    Assertions.assertEquals(tag3.name(), respTag2.name());
+    Assertions.assertEquals(tag3.comment(), respTag2.comment());
+    Assertions.assertFalse(respTag2.inherited().get());
+
+    // Test column tag
+    Response response3 =
+        target(basePath(metalake))
+            .path(column.type().toString())
+            .path(column.fullName())
+            .path("tags")
+            .path("tag4")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response3.getStatus());
+
+    TagResponse tagResponse3 = response3.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse3.getCode());
+
+    Tag respTag3 = tagResponse3.getTag();
+    Assertions.assertEquals(tag4.name(), respTag3.name());
+    Assertions.assertEquals(tag4.comment(), respTag3.comment());
+    Assertions.assertFalse(respTag3.inherited().get());
+
+    // Test get schema inherited tag
+    Response response4 =
+        target(basePath(metalake))
+            .path(schema.type().toString())
+            .path(schema.fullName())
+            .path("tags")
+            .path("tag1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response4.getStatus());
+
+    TagResponse tagResponse4 = response4.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse4.getCode());
+
+    Tag respTag4 = tagResponse4.getTag();
+    Assertions.assertEquals(tag1.name(), respTag4.name());
+    Assertions.assertEquals(tag1.comment(), respTag4.comment());
+    Assertions.assertTrue(respTag4.inherited().get());
+
+    // Test get table inherited tag
+    Response response5 =
+        target(basePath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("tags")
+            .path("tag2")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response5.getStatus());
+
+    TagResponse tagResponse5 = response5.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse5.getCode());
+
+    Tag respTag5 = tagResponse5.getTag();
+    Assertions.assertEquals(tag2.name(), respTag5.name());
+    Assertions.assertEquals(tag2.comment(), respTag5.comment());
+    Assertions.assertTrue(respTag5.inherited().get());
+
+    // Test get column inherited tag
+    Response response6 =
+        target(basePath(metalake))
+            .path(column.type().toString())
+            .path(column.fullName())
+            .path("tags")
+            .path("tag3")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response6.getStatus());
+
+    TagResponse tagResponse6 = response6.readEntity(TagResponse.class);
+    Assertions.assertEquals(0, tagResponse6.getCode());
+
+    Tag respTag6 = tagResponse6.getTag();
+    Assertions.assertEquals(tag3.name(), respTag6.name());
+    Assertions.assertEquals(tag3.comment(), respTag6.comment());
+    Assertions.assertTrue(respTag6.inherited().get());
+
+    // Test catalog tag throw NoSuchTagException
+    Response response7 =
+        target(basePath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("tags")
+            .path("tag2")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response7.getStatus());
+
+    ErrorResponse errorResponse = response7.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResponse.getCode());
+    Assertions.assertEquals(NoSuchTagException.class.getSimpleName(), 
errorResponse.getType());
+
+    // Test schema tag throw NoSuchTagException
+    Response response8 =
+        target(basePath(metalake))
+            .path(schema.type().toString())
+            .path(schema.fullName())
+            .path("tags")
+            .path("tag3")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response8.getStatus());
+
+    ErrorResponse errorResponse1 = response8.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResponse1.getCode());
+    Assertions.assertEquals(NoSuchTagException.class.getSimpleName(), 
errorResponse1.getType());
+  }
+
+  @Test
+  public void testAssociateTagsForObject() {
+    String[] tagsToAdd = new String[] {"tag1", "tag2"};
+    String[] tagsToRemove = new String[] {"tag3", "tag4"};
+
+    MetadataObject catalog = MetadataObjects.parse("object1", 
MetadataObject.Type.CATALOG);
+    when(tagManager.associateTagsForMetadataObject(metalake, catalog, 
tagsToAdd, tagsToRemove))
+        .thenReturn(tagsToAdd);
+
+    TagsAssociateRequest request = new TagsAssociateRequest(tagsToAdd, 
tagsToRemove);
+    Response response =
+        target(basePath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("tags")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
response.getMediaType());
+
+    NameListResponse nameListResponse = 
response.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse.getCode());
+
+    Assertions.assertArrayEquals(tagsToAdd, nameListResponse.getNames());
+
+    // Test throw null tags
+    when(tagManager.associateTagsForMetadataObject(metalake, catalog, 
tagsToAdd, tagsToRemove))
+        .thenReturn(null);
+    Response response1 =
+        target(basePath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("tags")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response1.getStatus());
+
+    NameListResponse nameListResponse1 = 
response1.readEntity(NameListResponse.class);
+    Assertions.assertEquals(0, nameListResponse1.getCode());
+
+    Assertions.assertEquals(0, nameListResponse1.getNames().length);
+
+    // Test throw TagAlreadyAssociatedException
+    doThrow(new TagAlreadyAssociatedException("mock error"))
+        .when(tagManager)
+        .associateTagsForMetadataObject(metalake, catalog, tagsToAdd, 
tagsToRemove);
+    Response response2 =
+        target(basePath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("tags")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), 
response2.getStatus());
+
+    ErrorResponse errorResponse = response2.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.ALREADY_EXISTS_CODE, 
errorResponse.getCode());
+    Assertions.assertEquals(
+        TagAlreadyAssociatedException.class.getSimpleName(), 
errorResponse.getType());
+
+    // Test throw RuntimeException
+    doThrow(new RuntimeException("mock error"))
+        .when(tagManager)
+        .associateTagsForMetadataObject(any(), any(), any(), any());
+
+    Response response3 =
+        target(basePath(metalake))
+            .path(catalog.type().toString())
+            .path(catalog.fullName())
+            .path("tags")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
response3.getStatus());
+
+    ErrorResponse errorResponse1 = response3.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResponse1.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResponse1.getType());
+  }
+
+  private String basePath(String metalake) {
+    return "/metalakes/" + metalake + "/objects";
+  }
+}

Reply via email to