This is an automated email from the ASF dual-hosted git repository.
yuqi4733 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 525a53f64 [#4172] feat(client-java): Add client side Java REST API for
Tag System (#4235)
525a53f64 is described below
commit 525a53f64a1ce694cef7761d77e5daaa1568dacc
Author: Jerry Shao <[email protected]>
AuthorDate: Wed Jul 24 15:20:13 2024 +0800
[#4172] feat(client-java): Add client side Java REST API for Tag System
(#4235)
### What changes were proposed in this pull request?
This PR proposes to add client side Java REST API for tag system.
### Why are the changes needed?
This is a part of work for tag system.
Fix: #4172
### Does this PR introduce _any_ user-facing change?
Yes.
### How was this patch tested?
UT added.
---
.../main/java/org/apache/gravitino/Catalog.java | 9 +
api/src/main/java/org/apache/gravitino/Schema.java | 9 +
.../java/org/apache/gravitino/file/Fileset.java | 9 +
.../java/org/apache/gravitino/messaging/Topic.java | 9 +
.../main/java/org/apache/gravitino/rel/Table.java | 9 +
.../org/apache/gravitino/tag/TagOperations.java | 8 +-
.../apache/gravitino/client/BaseSchemaCatalog.java | 46 +-
.../org/apache/gravitino/client/DTOConverters.java | 25 +
.../org/apache/gravitino/client/ErrorHandlers.java | 56 +++
.../apache/gravitino/client/FilesetCatalog.java | 9 +-
.../apache/gravitino/client/GenericFileset.java | 124 +++++
.../org/apache/gravitino/client/GenericSchema.java | 107 +++++
.../org/apache/gravitino/client/GenericTag.java | 105 +++++
.../org/apache/gravitino/client/GenericTopic.java | 112 +++++
.../apache/gravitino/client/GravitinoClient.java | 40 +-
.../gravitino/client/GravitinoClientBase.java | 6 +
.../apache/gravitino/client/GravitinoMetalake.java | 160 ++++++-
.../apache/gravitino/client/MessagingCatalog.java | 8 +-
.../client/MetadataObjectTagOperations.java | 119 +++++
.../apache/gravitino/client/RelationalCatalog.java | 2 +-
.../apache/gravitino/client/RelationalTable.java | 60 ++-
.../apache/gravitino/client/TestGenericTag.java | 147 ++++++
...noClient.java => TestGravitinoAdminClient.java} | 2 +-
.../gravitino/client/TestGravitinoMetalake.java | 311 +++++++++++++
.../apache/gravitino/client/TestSupportTags.java | 503 +++++++++++++++++++++
.../java/org/apache/gravitino/dto/SchemaDTO.java | 2 +
.../org/apache/gravitino/dto/file/FilesetDTO.java | 2 +
.../java/org/apache/gravitino/dto/tag/TagDTO.java | 24 +-
28 files changed, 1990 insertions(+), 33 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/Catalog.java
b/api/src/main/java/org/apache/gravitino/Catalog.java
index 4809c7ead..bbcc0cb7d 100644
--- a/api/src/main/java/org/apache/gravitino/Catalog.java
+++ b/api/src/main/java/org/apache/gravitino/Catalog.java
@@ -23,6 +23,7 @@ import org.apache.gravitino.annotation.Evolving;
import org.apache.gravitino.file.FilesetCatalog;
import org.apache.gravitino.messaging.TopicCatalog;
import org.apache.gravitino.rel.TableCatalog;
+import org.apache.gravitino.tag.SupportsTags;
/**
* The interface of a catalog. The catalog is the second level entity in the
Gravitino system,
@@ -152,4 +153,12 @@ public interface Catalog extends Auditable {
default TopicCatalog asTopicCatalog() throws UnsupportedOperationException {
throw new UnsupportedOperationException("Catalog does not support topic
operations");
}
+
+ /**
+ * @return the {@link SupportsTags} if the catalog supports tag operations.
+ * @throws UnsupportedOperationException if the catalog does not support tag
operations.
+ */
+ default SupportsTags supportsTags() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("Catalog does not support tag
operations");
+ }
}
diff --git a/api/src/main/java/org/apache/gravitino/Schema.java
b/api/src/main/java/org/apache/gravitino/Schema.java
index e835ee685..872b0a25e 100644
--- a/api/src/main/java/org/apache/gravitino/Schema.java
+++ b/api/src/main/java/org/apache/gravitino/Schema.java
@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.Map;
import javax.annotation.Nullable;
import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.tag.SupportsTags;
/**
* An interface representing a schema in the {@link Catalog}. A Schema is a
basic container of
@@ -47,4 +48,12 @@ public interface Schema extends Auditable {
default Map<String, String> properties() {
return Collections.emptyMap();
}
+
+ /**
+ * @return the {@link SupportsTags} if the schema supports tag operations.
+ * @throws UnsupportedOperationException if the schema does not support tag
operations.
+ */
+ default SupportsTags supportsTags() {
+ throw new UnsupportedOperationException("Schema does not support tag
operations.");
+ }
}
diff --git a/api/src/main/java/org/apache/gravitino/file/Fileset.java
b/api/src/main/java/org/apache/gravitino/file/Fileset.java
index 884cb20a9..ccff039da 100644
--- a/api/src/main/java/org/apache/gravitino/file/Fileset.java
+++ b/api/src/main/java/org/apache/gravitino/file/Fileset.java
@@ -24,6 +24,7 @@ import javax.annotation.Nullable;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.tag.SupportsTags;
/**
* An interface representing a fileset under a schema {@link Namespace}. A
fileset is a virtual
@@ -105,4 +106,12 @@ public interface Fileset extends Auditable {
default Map<String, String> properties() {
return Collections.emptyMap();
}
+
+ /**
+ * @return The {@link SupportsTags} if the fileset supports tag operations.
+ * @throws UnsupportedOperationException If the fileset does not support tag
operations.
+ */
+ default SupportsTags supportsTags() {
+ throw new UnsupportedOperationException("Fileset does not support tag
operations.");
+ }
}
diff --git a/api/src/main/java/org/apache/gravitino/messaging/Topic.java
b/api/src/main/java/org/apache/gravitino/messaging/Topic.java
index b086f12a0..78607f486 100644
--- a/api/src/main/java/org/apache/gravitino/messaging/Topic.java
+++ b/api/src/main/java/org/apache/gravitino/messaging/Topic.java
@@ -24,6 +24,7 @@ import javax.annotation.Nullable;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.tag.SupportsTags;
/**
* An interface representing a topic under a schema {@link Namespace}. A topic
is a message queue
@@ -49,4 +50,12 @@ public interface Topic extends Auditable {
default Map<String, String> properties() {
return Collections.emptyMap();
}
+
+ /**
+ * @return the {@link SupportsTags} if the topic supports tag operations.
+ * @throws UnsupportedOperationException if the topic does not support tag
operations.
+ */
+ default SupportsTags supportsTags() {
+ throw new UnsupportedOperationException("Topic does not support tag
operations.");
+ }
}
diff --git a/api/src/main/java/org/apache/gravitino/rel/Table.java
b/api/src/main/java/org/apache/gravitino/rel/Table.java
index 8d542fed7..c6bafb97a 100644
--- a/api/src/main/java/org/apache/gravitino/rel/Table.java
+++ b/api/src/main/java/org/apache/gravitino/rel/Table.java
@@ -31,6 +31,7 @@ import
org.apache.gravitino.rel.expressions.transforms.Transform;
import org.apache.gravitino.rel.expressions.transforms.Transforms;
import org.apache.gravitino.rel.indexes.Index;
import org.apache.gravitino.rel.indexes.Indexes;
+import org.apache.gravitino.tag.SupportsTags;
/**
* An interface representing a table in a {@link Namespace}. It defines the
basic properties of a
@@ -94,4 +95,12 @@ public interface Table extends Auditable {
default SupportsPartitions supportPartitions() throws
UnsupportedOperationException {
throw new UnsupportedOperationException("Table does not support partition
operations.");
}
+
+ /**
+ * @return The {@link SupportsTags} if the table supports tag operations.
+ * @throws UnsupportedOperationException If the table does not support tag
operations.
+ */
+ default SupportsTags supportsTags() {
+ throw new UnsupportedOperationException("Table does not support tag
operations.");
+ }
}
diff --git a/api/src/main/java/org/apache/gravitino/tag/TagOperations.java
b/api/src/main/java/org/apache/gravitino/tag/TagOperations.java
index 3674e6cc7..1745efb23 100644
--- a/api/src/main/java/org/apache/gravitino/tag/TagOperations.java
+++ b/api/src/main/java/org/apache/gravitino/tag/TagOperations.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.tag;
import java.util.Map;
import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.exceptions.NoSuchMetalakeException;
import org.apache.gravitino.exceptions.NoSuchTagException;
import org.apache.gravitino.exceptions.TagAlreadyExistsException;
@@ -36,16 +37,17 @@ public interface TagOperations {
* List all the tag names under a metalake.
*
* @return The list of tag names.
+ * @throws NoSuchMetalakeException If the metalake does not exist.
*/
- String[] listTags();
+ String[] listTags() throws NoSuchMetalakeException;
/**
* List all the tags with detailed information under a metalake.
*
- * @param extended If true, the extended information of the tag will be
included.
* @return The list of tags.
+ * @throws NoSuchMetalakeException If the metalake does not exist.
*/
- Tag[] listTagsInfo(boolean extended);
+ Tag[] listTagsInfo() throws NoSuchMetalakeException;
/**
* Get a tag by its name under a metalake.
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
index 96313c848..7d46af3a5 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
@@ -24,6 +24,8 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.gravitino.Catalog;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.Schema;
@@ -42,19 +44,24 @@ import
org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.NonEmptySchemaException;
import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.tag.SupportsTags;
+import org.apache.gravitino.tag.Tag;
/**
* BaseSchemaCatalog is the base abstract class for all the catalog with
schema. It provides the
* common methods for managing schemas in a catalog. With {@link
BaseSchemaCatalog}, users can list,
* create, load, alter and drop a schema with specified identifier.
*/
-abstract class BaseSchemaCatalog extends CatalogDTO implements Catalog,
SupportsSchemas {
+abstract class BaseSchemaCatalog extends CatalogDTO
+ implements Catalog, SupportsSchemas, SupportsTags {
/** The REST client to send the requests. */
protected final RESTClient restClient;
/** The namespace of current catalog, which is the metalake name. */
private final Namespace catalogNamespace;
+ private final MetadataObjectTagOperations objectTagOperations;
+
BaseSchemaCatalog(
Namespace catalogNamespace,
String name,
@@ -65,12 +72,18 @@ abstract class BaseSchemaCatalog extends CatalogDTO
implements Catalog, Supports
AuditDTO auditDTO,
RESTClient restClient) {
super(name, type, provider, comment, properties, auditDTO);
+
this.restClient = restClient;
Namespace.check(
catalogNamespace != null && catalogNamespace.length() == 1,
"Catalog namespace must be non-null and have 1 level, the input
namespace is %s",
catalogNamespace);
this.catalogNamespace = catalogNamespace;
+
+ MetadataObject metadataObject =
+ MetadataObjects.of(null, this.name(), MetadataObject.Type.CATALOG);
+ this.objectTagOperations =
+ new MetadataObjectTagOperations(catalogNamespace.level(0),
metadataObject, restClient);
}
@Override
@@ -78,6 +91,11 @@ abstract class BaseSchemaCatalog extends CatalogDTO
implements Catalog, Supports
return this;
}
+ @Override
+ public SupportsTags supportsTags() throws UnsupportedOperationException {
+ return this;
+ }
+
/**
* List all the schemas under the given catalog namespace.
*
@@ -125,7 +143,7 @@ abstract class BaseSchemaCatalog extends CatalogDTO
implements Catalog, Supports
ErrorHandlers.schemaErrorHandler());
resp.validate();
- return resp.getSchema();
+ return new GenericSchema(resp.getSchema(), restClient,
catalogNamespace.level(0), this.name());
}
/**
@@ -146,7 +164,7 @@ abstract class BaseSchemaCatalog extends CatalogDTO
implements Catalog, Supports
ErrorHandlers.schemaErrorHandler());
resp.validate();
- return resp.getSchema();
+ return new GenericSchema(resp.getSchema(), restClient,
catalogNamespace.level(0), this.name());
}
/**
@@ -177,7 +195,7 @@ abstract class BaseSchemaCatalog extends CatalogDTO
implements Catalog, Supports
ErrorHandlers.schemaErrorHandler());
resp.validate();
- return resp.getSchema();
+ return new GenericSchema(resp.getSchema(), restClient,
catalogNamespace.level(0), this.name());
}
/**
@@ -201,6 +219,26 @@ abstract class BaseSchemaCatalog extends CatalogDTO
implements Catalog, Supports
return resp.dropped();
}
+ @Override
+ public String[] listTags() {
+ return objectTagOperations.listTags();
+ }
+
+ @Override
+ public Tag[] listTagsInfo() {
+ return objectTagOperations.listTagsInfo();
+ }
+
+ @Override
+ public Tag getTag(String name) {
+ return objectTagOperations.getTag(name);
+ }
+
+ @Override
+ public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove) {
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
/**
* Get the namespace of the current catalog, which is "metalake".
*
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
index 4e60ed079..6b259405d 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
@@ -36,12 +36,14 @@ import
org.apache.gravitino.dto.requests.FilesetUpdateRequest;
import org.apache.gravitino.dto.requests.MetalakeUpdateRequest;
import org.apache.gravitino.dto.requests.SchemaUpdateRequest;
import org.apache.gravitino.dto.requests.TableUpdateRequest;
+import org.apache.gravitino.dto.requests.TagUpdateRequest;
import org.apache.gravitino.dto.requests.TopicUpdateRequest;
import org.apache.gravitino.file.FilesetChange;
import org.apache.gravitino.messaging.TopicChange;
import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.TableChange;
import org.apache.gravitino.rel.expressions.Expression;
+import org.apache.gravitino.tag.TagChange;
class DTOConverters {
private DTOConverters() {}
@@ -312,4 +314,27 @@ class DTOConverters {
.toArray(PrivilegeDTO[]::new))
.build();
}
+
+ static TagUpdateRequest toTagUpdateRequest(TagChange change) {
+ if (change instanceof TagChange.RenameTag) {
+ return new TagUpdateRequest.RenameTagRequest(((TagChange.RenameTag)
change).getNewName());
+
+ } else if (change instanceof TagChange.UpdateTagComment) {
+ return new TagUpdateRequest.UpdateTagCommentRequest(
+ ((TagChange.UpdateTagComment) change).getNewComment());
+
+ } else if (change instanceof TagChange.SetProperty) {
+ return new TagUpdateRequest.SetTagPropertyRequest(
+ ((TagChange.SetProperty) change).getProperty(),
+ ((TagChange.SetProperty) change).getValue());
+
+ } else if (change instanceof TagChange.RemoveProperty) {
+ return new TagUpdateRequest.RemoveTagPropertyRequest(
+ ((TagChange.RemoveProperty) change).getProperty());
+
+ } else {
+ throw new IllegalArgumentException(
+ "Unknown change type: " + change.getClass().getSimpleName());
+ }
+ }
}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
index ae87cf6e1..f98ee7708 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
@@ -25,6 +25,7 @@ import java.util.function.Consumer;
import org.apache.gravitino.dto.responses.ErrorConstants;
import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.dto.responses.OAuth2ErrorResponse;
+import org.apache.gravitino.exceptions.AlreadyExistsException;
import org.apache.gravitino.exceptions.BadRequestException;
import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
import org.apache.gravitino.exceptions.ConnectionFailedException;
@@ -39,6 +40,7 @@ import
org.apache.gravitino.exceptions.NoSuchPartitionException;
import org.apache.gravitino.exceptions.NoSuchRoleException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.exceptions.NoSuchTagException;
import org.apache.gravitino.exceptions.NoSuchTopicException;
import org.apache.gravitino.exceptions.NoSuchUserException;
import org.apache.gravitino.exceptions.NonEmptySchemaException;
@@ -48,6 +50,8 @@ import org.apache.gravitino.exceptions.RESTException;
import org.apache.gravitino.exceptions.RoleAlreadyExistsException;
import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
import org.apache.gravitino.exceptions.TableAlreadyExistsException;
+import org.apache.gravitino.exceptions.TagAlreadyAssociatedException;
+import org.apache.gravitino.exceptions.TagAlreadyExistsException;
import org.apache.gravitino.exceptions.TopicAlreadyExistsException;
import org.apache.gravitino.exceptions.UnauthorizedException;
import org.apache.gravitino.exceptions.UserAlreadyExistsException;
@@ -175,6 +179,15 @@ public class ErrorHandlers {
return PermissionOperationErrorHandler.INSTANCE;
}
+ /**
+ * Creates an error handler specific to Tag operations.
+ *
+ * @return A Consumer representing the Tag error handler.
+ */
+ public static Consumer<ErrorResponse> tagErrorHandler() {
+ return TagErrorHandler.INSTANCE;
+ }
+
private ErrorHandlers() {}
/**
@@ -657,6 +670,49 @@ public class ErrorHandlers {
}
}
+ /** Error handler specific to Tag operations. */
+ @SuppressWarnings("FormatStringAnnotation")
+ private static class TagErrorHandler extends RestErrorHandler {
+
+ private static final TagErrorHandler INSTANCE = new TagErrorHandler();
+
+ @Override
+ public void accept(ErrorResponse errorResponse) {
+ String errorMessage = formatErrorMessage(errorResponse);
+
+ switch (errorResponse.getCode()) {
+ case ErrorConstants.ILLEGAL_ARGUMENTS_CODE:
+ throw new IllegalArgumentException(errorMessage);
+
+ case ErrorConstants.NOT_FOUND_CODE:
+ if
(errorResponse.getType().equals(NoSuchMetalakeException.class.getSimpleName()))
{
+ throw new NoSuchMetalakeException(errorMessage);
+ } else if
(errorResponse.getType().equals(NoSuchTagException.class.getSimpleName())) {
+ throw new NoSuchTagException(errorMessage);
+ } else {
+ throw new NotFoundException(errorMessage);
+ }
+
+ case ErrorConstants.ALREADY_EXISTS_CODE:
+ if
(errorResponse.getType().equals(TagAlreadyExistsException.class.getSimpleName()))
{
+ throw new TagAlreadyExistsException(errorMessage);
+ } else if (errorResponse
+ .getType()
+ .equals(TagAlreadyAssociatedException.class.getSimpleName())) {
+ throw new TagAlreadyAssociatedException(errorMessage);
+ } else {
+ throw new AlreadyExistsException(errorMessage);
+ }
+
+ case ErrorConstants.INTERNAL_ERROR_CODE:
+ throw new RuntimeException(errorMessage);
+
+ default:
+ super.accept(errorResponse);
+ }
+ }
+ }
+
/** Generic error handler for REST requests. */
private static class RestErrorHandler extends ErrorHandler {
private static final ErrorHandler INSTANCE = new RestErrorHandler();
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
index 6aa462470..3f0d49530 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
@@ -49,8 +49,7 @@ import org.apache.gravitino.rest.RESTUtils;
* example, schemas and filesets list, creation, update and deletion. A
Fileset catalog is under the
* metalake.
*/
-public class FilesetCatalog extends BaseSchemaCatalog
- implements org.apache.gravitino.file.FilesetCatalog {
+class FilesetCatalog extends BaseSchemaCatalog implements
org.apache.gravitino.file.FilesetCatalog {
FilesetCatalog(
Namespace namespace,
@@ -116,7 +115,7 @@ public class FilesetCatalog extends BaseSchemaCatalog
ErrorHandlers.filesetErrorHandler());
resp.validate();
- return resp.getFileset();
+ return new GenericFileset(resp.getFileset(), restClient, fullNamespace);
}
/**
@@ -165,7 +164,7 @@ public class FilesetCatalog extends BaseSchemaCatalog
ErrorHandlers.filesetErrorHandler());
resp.validate();
- return resp.getFileset();
+ return new GenericFileset(resp.getFileset(), restClient, fullNamespace);
}
/**
@@ -199,7 +198,7 @@ public class FilesetCatalog extends BaseSchemaCatalog
ErrorHandlers.filesetErrorHandler());
resp.validate();
- return resp.getFileset();
+ return new GenericFileset(resp.getFileset(), restClient, fullNamespace);
}
/**
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
new file mode 100644
index 000000000..32e1d7392
--- /dev/null
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
@@ -0,0 +1,124 @@
+/*
+ * 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.client;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.dto.file.FilesetDTO;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.tag.SupportsTags;
+import org.apache.gravitino.tag.Tag;
+
+/** Represents a generic fileset. */
+class GenericFileset implements Fileset, SupportsTags {
+
+ private final FilesetDTO filesetDTO;
+
+ private final MetadataObjectTagOperations objectTagOperations;
+
+ GenericFileset(FilesetDTO filesetDTO, RESTClient restClient, Namespace
filesetNs) {
+ this.filesetDTO = filesetDTO;
+ List<String> filesetFullName =
+ Lists.newArrayList(filesetNs.level(1), filesetNs.level(2),
filesetDTO.name());
+ MetadataObject filesetObject = MetadataObjects.of(filesetFullName,
MetadataObject.Type.FILESET);
+ this.objectTagOperations =
+ new MetadataObjectTagOperations(filesetNs.level(0), filesetObject,
restClient);
+ }
+
+ @Override
+ public Audit auditInfo() {
+ return filesetDTO.auditInfo();
+ }
+
+ @Override
+ public String name() {
+ return filesetDTO.name();
+ }
+
+ @Nullable
+ @Override
+ public String comment() {
+ return filesetDTO.comment();
+ }
+
+ @Override
+ public Type type() {
+ return filesetDTO.type();
+ }
+
+ @Override
+ public String storageLocation() {
+ return filesetDTO.storageLocation();
+ }
+
+ @Override
+ public Map<String, String> properties() {
+ return filesetDTO.properties();
+ }
+
+ @Override
+ public SupportsTags supportsTags() {
+ return this;
+ }
+
+ @Override
+ public String[] listTags() {
+ return objectTagOperations.listTags();
+ }
+
+ @Override
+ public Tag[] listTagsInfo() {
+ return objectTagOperations.listTagsInfo();
+ }
+
+ @Override
+ public Tag getTag(String name) throws NoSuchTagException {
+ return objectTagOperations.getTag(name);
+ }
+
+ @Override
+ public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove) {
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof GenericFileset)) {
+ return false;
+ }
+
+ GenericFileset that = (GenericFileset) obj;
+ return filesetDTO.equals(that.filesetDTO);
+ }
+
+ @Override
+ public int hashCode() {
+ return filesetDTO.hashCode();
+ }
+}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
new file mode 100644
index 000000000..e595a53ab
--- /dev/null
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
@@ -0,0 +1,107 @@
+/*
+ * 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.client;
+
+import java.util.Map;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.dto.SchemaDTO;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.tag.SupportsTags;
+import org.apache.gravitino.tag.Tag;
+
+/** Represents a generic schema. */
+class GenericSchema implements Schema, SupportsTags {
+
+ private final SchemaDTO schemaDTO;
+
+ private final MetadataObjectTagOperations objectTagOperations;
+
+ GenericSchema(SchemaDTO schemaDTO, RESTClient restClient, String metalake,
String catalog) {
+ this.schemaDTO = schemaDTO;
+ MetadataObject schemaObject =
+ MetadataObjects.of(catalog, schemaDTO.name(),
MetadataObject.Type.SCHEMA);
+ this.objectTagOperations = new MetadataObjectTagOperations(metalake,
schemaObject, restClient);
+ }
+
+ @Override
+ public SupportsTags supportsTags() {
+ return this;
+ }
+
+ @Override
+ public String name() {
+ return schemaDTO.name();
+ }
+
+ @Override
+ public String comment() {
+ return schemaDTO.comment();
+ }
+
+ @Override
+ public Map<String, String> properties() {
+ return schemaDTO.properties();
+ }
+
+ @Override
+ public Audit auditInfo() {
+ return schemaDTO.auditInfo();
+ }
+
+ @Override
+ public String[] listTags() {
+ return objectTagOperations.listTags();
+ }
+
+ @Override
+ public Tag[] listTagsInfo() {
+ return objectTagOperations.listTagsInfo();
+ }
+
+ @Override
+ public Tag getTag(String name) throws NoSuchTagException {
+ return objectTagOperations.getTag(name);
+ }
+
+ @Override
+ public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove) {
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof GenericSchema)) {
+ return false;
+ }
+
+ GenericSchema that = (GenericSchema) obj;
+ return schemaDTO.equals(that.schemaDTO);
+ }
+
+ @Override
+ public int hashCode() {
+ return schemaDTO.hashCode();
+ }
+}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTag.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTag.java
new file mode 100644
index 000000000..19ca417b3
--- /dev/null
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTag.java
@@ -0,0 +1,105 @@
+/*
+ * 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.client;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.dto.responses.MetadataObjectListResponse;
+import org.apache.gravitino.dto.tag.TagDTO;
+import org.apache.gravitino.tag.Tag;
+
+/** Represents a generic tag. */
+class GenericTag implements Tag, Tag.AssociatedObjects {
+
+ private final TagDTO tagDTO;
+
+ private final RESTClient restClient;
+
+ private final String metalake;
+
+ GenericTag(TagDTO tagDTO, RESTClient restClient, String metalake) {
+ this.tagDTO = tagDTO;
+ this.restClient = restClient;
+ this.metalake = metalake;
+ }
+
+ @Override
+ public String name() {
+ return tagDTO.name();
+ }
+
+ @Override
+ public String comment() {
+ return tagDTO.comment();
+ }
+
+ @Override
+ public Map<String, String> properties() {
+ return tagDTO.properties();
+ }
+
+ @Override
+ public Optional<Boolean> inherited() {
+ return tagDTO.inherited();
+ }
+
+ @Override
+ public Audit auditInfo() {
+ return tagDTO.auditInfo();
+ }
+
+ @Override
+ public AssociatedObjects associatedObjects() {
+ return this;
+ }
+
+ @Override
+ public MetadataObject[] objects() {
+ MetadataObjectListResponse resp =
+ restClient.get(
+ String.format("api/metalakes/%s/tags/%s/objects", metalake,
name()),
+ MetadataObjectListResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+
+ resp.validate();
+ return resp.getMetadataObjects();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof GenericTag)) {
+ return false;
+ }
+
+ GenericTag that = (GenericTag) obj;
+ return tagDTO.equals(that.tagDTO);
+ }
+
+ @Override
+ public int hashCode() {
+ return tagDTO.hashCode();
+ }
+}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTopic.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTopic.java
new file mode 100644
index 000000000..e317df069
--- /dev/null
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTopic.java
@@ -0,0 +1,112 @@
+/*
+ * 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.client;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.dto.messaging.TopicDTO;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.messaging.Topic;
+import org.apache.gravitino.tag.SupportsTags;
+import org.apache.gravitino.tag.Tag;
+
+/** Represents a generic topic. */
+class GenericTopic implements Topic, SupportsTags {
+
+ private final TopicDTO topicDTO;
+
+ private final MetadataObjectTagOperations objectTagOperations;
+
+ GenericTopic(TopicDTO topicDTO, RESTClient restClient, Namespace topicNs) {
+ this.topicDTO = topicDTO;
+ List<String> topicFullName =
+ Lists.newArrayList(topicNs.level(1), topicNs.level(2),
topicDTO.name());
+ MetadataObject topicObject = MetadataObjects.of(topicFullName,
MetadataObject.Type.TOPIC);
+ this.objectTagOperations =
+ new MetadataObjectTagOperations(topicNs.level(0), topicObject,
restClient);
+ }
+
+ @Override
+ public Audit auditInfo() {
+ return null;
+ }
+
+ @Override
+ public String name() {
+ return topicDTO.name();
+ }
+
+ @Override
+ public String comment() {
+ return topicDTO.comment();
+ }
+
+ @Override
+ public Map<String, String> properties() {
+ return topicDTO.properties();
+ }
+
+ @Override
+ public SupportsTags supportsTags() {
+ return this;
+ }
+
+ @Override
+ public String[] listTags() {
+ return objectTagOperations.listTags();
+ }
+
+ @Override
+ public Tag[] listTagsInfo() {
+ return objectTagOperations.listTagsInfo();
+ }
+
+ @Override
+ public Tag getTag(String name) throws NoSuchTagException {
+ return objectTagOperations.getTag(name);
+ }
+
+ @Override
+ public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove) {
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof GenericTopic)) {
+ return false;
+ }
+
+ GenericTopic that = (GenericTopic) obj;
+ return topicDTO.equals(that.topicDTO);
+ }
+
+ @Override
+ public int hashCode() {
+ return topicDTO.hashCode();
+ }
+}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
index 1311930b8..5a233ee11 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
@@ -27,6 +27,11 @@ import org.apache.gravitino.SupportsCatalogs;
import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.exceptions.TagAlreadyExistsException;
+import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagChange;
+import org.apache.gravitino.tag.TagOperations;
/**
* Apache Gravitino Client for an user to interact with the Gravitino API,
allowing the client to
@@ -35,7 +40,8 @@ import
org.apache.gravitino.exceptions.NoSuchMetalakeException;
* <p>It uses an underlying {@link RESTClient} to send HTTP requests and
receive responses from the
* API.
*/
-public class GravitinoClient extends GravitinoClientBase implements
SupportsCatalogs {
+public class GravitinoClient extends GravitinoClientBase
+ implements SupportsCatalogs, TagOperations {
private final GravitinoMetalake metalake;
@@ -139,6 +145,38 @@ public class GravitinoClient extends GravitinoClientBase
implements SupportsCata
getMetalake().testConnection(catalogName, type, provider, comment,
properties);
}
+ @Override
+ public String[] listTags() throws NoSuchMetalakeException {
+ return getMetalake().listTags();
+ }
+
+ @Override
+ public Tag[] listTagsInfo() throws NoSuchMetalakeException {
+ return getMetalake().listTagsInfo();
+ }
+
+ @Override
+ public Tag getTag(String name) throws NoSuchTagException {
+ return getMetalake().getTag(name);
+ }
+
+ @Override
+ public Tag createTag(String name, String comment, Map<String, String>
properties)
+ throws TagAlreadyExistsException {
+ return getMetalake().createTag(name, comment, properties);
+ }
+
+ @Override
+ public Tag alterTag(String name, TagChange... changes)
+ throws NoSuchTagException, IllegalArgumentException {
+ return getMetalake().alterTag(name, changes);
+ }
+
+ @Override
+ public boolean deleteTag(String name) {
+ return getMetalake().deleteTag(name);
+ }
+
/** Builder class for constructing a GravitinoClient. */
public static class ClientBuilder extends
GravitinoClientBase.Builder<GravitinoClient> {
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClientBase.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClientBase.java
index de18b535d..26ab962d6 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClientBase.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClientBase.java
@@ -20,6 +20,7 @@
package org.apache.gravitino.client;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.InlineMe;
import java.io.Closeable;
@@ -188,6 +189,11 @@ public abstract class GravitinoClientBase implements
Closeable {
}
}
+ @VisibleForTesting
+ RESTClient restClient() {
+ return restClient;
+ }
+
/** Builder class for constructing a GravitinoClient. */
public abstract static class Builder<T> {
/** The base URI for the Gravitino API. */
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
index f8a95aa9b..0c908a506 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.client;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -35,23 +36,36 @@ import org.apache.gravitino.dto.MetalakeDTO;
import org.apache.gravitino.dto.requests.CatalogCreateRequest;
import org.apache.gravitino.dto.requests.CatalogUpdateRequest;
import org.apache.gravitino.dto.requests.CatalogUpdatesRequest;
+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.responses.CatalogListResponse;
import org.apache.gravitino.dto.responses.CatalogResponse;
import org.apache.gravitino.dto.responses.DropResponse;
import org.apache.gravitino.dto.responses.EntityListResponse;
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.CatalogAlreadyExistsException;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.exceptions.TagAlreadyExistsException;
+import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagChange;
+import org.apache.gravitino.tag.TagOperations;
/**
* Apache Gravitino Metalake is the top-level metadata repository for users.
It contains a list of
* catalogs as sub-level metadata collections. With {@link GravitinoMetalake},
users can list,
* create, load, alter and drop a catalog with specified identifier.
*/
-public class GravitinoMetalake extends MetalakeDTO implements SupportsCatalogs
{
+public class GravitinoMetalake extends MetalakeDTO implements
SupportsCatalogs, TagOperations {
private static final String API_METALAKES_CATALOGS_PATH =
"api/metalakes/%s/catalogs/%s";
+ private static final String API_METALAKES_TAGS_PATH =
"api/metalakes/%s/tags";
+
private final RESTClient restClient;
GravitinoMetalake(
@@ -149,7 +163,6 @@ public class GravitinoMetalake extends MetalakeDTO
implements SupportsCatalogs {
String comment,
Map<String, String> properties)
throws NoSuchMetalakeException, CatalogAlreadyExistsException {
-
CatalogCreateRequest req =
new CatalogCreateRequest(catalogName, type, provider, comment,
properties);
req.validate();
@@ -178,7 +191,6 @@ public class GravitinoMetalake extends MetalakeDTO
implements SupportsCatalogs {
@Override
public Catalog alterCatalog(String catalogName, CatalogChange... changes)
throws NoSuchCatalogException, IllegalArgumentException {
-
List<CatalogUpdateRequest> reqs =
Arrays.stream(changes)
.map(DTOConverters::toCatalogUpdateRequest)
@@ -206,7 +218,6 @@ public class GravitinoMetalake extends MetalakeDTO
implements SupportsCatalogs {
*/
@Override
public boolean dropCatalog(String catalogName) {
-
DropResponse resp =
restClient.delete(
String.format(API_METALAKES_CATALOGS_PATH, this.name(),
catalogName),
@@ -259,6 +270,147 @@ public class GravitinoMetalake extends MetalakeDTO
implements SupportsCatalogs {
ErrorHandlers.catalogErrorHandler().accept(resp);
}
+ /*
+ * List all the tag names under a metalake.
+ *
+ * @return A list of the tag names under the current metalake.
+ * @throws NoSuchMetalakeException If the metalake does not exist.
+ */
+ @Override
+ public String[] listTags() throws NoSuchMetalakeException {
+ NameListResponse resp =
+ restClient.get(
+ String.format(API_METALAKES_TAGS_PATH, this.name()),
+ NameListResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+ resp.validate();
+ return resp.getNames();
+ }
+
+ /**
+ * List all the tags with detailed information under the current metalake.
+ *
+ * @return A list of {@link Tag} under the current metalake.
+ * @throws NoSuchMetalakeException If the metalake does not exist.
+ */
+ @Override
+ public Tag[] listTagsInfo() throws NoSuchMetalakeException {
+ Map<String, String> params = ImmutableMap.of("details", "true");
+ TagListResponse resp =
+ restClient.get(
+ String.format(API_METALAKES_TAGS_PATH, this.name()),
+ params,
+ TagListResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+ resp.validate();
+
+ return Arrays.stream(resp.getTags())
+ .map(t -> new GenericTag(t, restClient, this.name()))
+ .toArray(Tag[]::new);
+ }
+
+ /**
+ * Get a tag by its name under the current metalake.
+ *
+ * @param name The name of the tag.
+ * @return The tag.
+ * @throws NoSuchTagException If the tag does not exist.
+ */
+ @Override
+ public Tag getTag(String name) throws NoSuchTagException {
+ Preconditions.checkArgument(StringUtils.isNotBlank(name), "tag name must
not be null or empty");
+
+ TagResponse resp =
+ restClient.get(
+ String.format(API_METALAKES_TAGS_PATH, this.name()) + "/" + name,
+ TagResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+ resp.validate();
+
+ return new GenericTag(resp.getTag(), restClient, this.name());
+ }
+
+ /**
+ * Create a tag under the current metalake.
+ *
+ * @param name The name of the tag.
+ * @param comment The comment of the tag.
+ * @param properties The properties of the tag.
+ * @return The created tag.
+ * @throws TagAlreadyExistsException If the tag already exists.
+ */
+ @Override
+ public Tag createTag(String name, String comment, Map<String, String>
properties)
+ throws TagAlreadyExistsException {
+ Preconditions.checkArgument(StringUtils.isNotBlank(name), "tag name must
not be null or empty");
+ TagCreateRequest req = new TagCreateRequest(name, comment, properties);
+ req.validate();
+
+ TagResponse resp =
+ restClient.post(
+ String.format(API_METALAKES_TAGS_PATH, this.name()),
+ req,
+ TagResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+ resp.validate();
+
+ return new GenericTag(resp.getTag(), restClient, this.name());
+ }
+
+ /**
+ * Alter a tag under the current metalake.
+ *
+ * @param name The name of the tag.
+ * @param changes The changes to apply to the tag.
+ * @return The altered tag.
+ * @throws NoSuchTagException If the tag does not exist.
+ * @throws IllegalArgumentException If the changes cannot be applied to the
tag.
+ */
+ @Override
+ public Tag alterTag(String name, TagChange... changes)
+ throws NoSuchTagException, IllegalArgumentException {
+ Preconditions.checkArgument(StringUtils.isNotBlank(name), "tag name must
not be null or empty");
+ List<TagUpdateRequest> updates =
+
Arrays.stream(changes).map(DTOConverters::toTagUpdateRequest).collect(Collectors.toList());
+ TagUpdatesRequest req = new TagUpdatesRequest(updates);
+ req.validate();
+
+ TagResponse resp =
+ restClient.put(
+ String.format(API_METALAKES_TAGS_PATH, this.name()) + "/" + name,
+ req,
+ TagResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+ resp.validate();
+
+ return new GenericTag(resp.getTag(), restClient, this.name());
+ }
+
+ /**
+ * Delete a tag under the current metalake.
+ *
+ * @param name The name of the tag.
+ * @return True if the tag is deleted, false if the tag does not exist.
+ */
+ @Override
+ public boolean deleteTag(String name) {
+ Preconditions.checkArgument(StringUtils.isNotBlank(name), "tag name must
not be null or empty");
+
+ DropResponse resp =
+ restClient.delete(
+ String.format(API_METALAKES_TAGS_PATH, this.name()) + "/" + name,
+ DropResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+ resp.validate();
+ return resp.dropped();
+ }
+
static class Builder extends MetalakeDTO.Builder<Builder> {
private RESTClient restClient;
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/MessagingCatalog.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/MessagingCatalog.java
index 9ff625644..c4e4199c4 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/MessagingCatalog.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/MessagingCatalog.java
@@ -50,7 +50,7 @@ import org.apache.gravitino.messaging.TopicChange;
* for example, topics list, creation, update and deletion. A Messaging
catalog is under the
* metalake.
*/
-public class MessagingCatalog extends BaseSchemaCatalog implements
TopicCatalog {
+class MessagingCatalog extends BaseSchemaCatalog implements TopicCatalog {
MessagingCatalog(
Namespace namespace,
@@ -121,7 +121,7 @@ public class MessagingCatalog extends BaseSchemaCatalog
implements TopicCatalog
ErrorHandlers.topicErrorHandler());
resp.validate();
- return resp.getTopic();
+ return new GenericTopic(resp.getTopic(), restClient, fullNamespace);
}
/**
@@ -160,7 +160,7 @@ public class MessagingCatalog extends BaseSchemaCatalog
implements TopicCatalog
ErrorHandlers.topicErrorHandler());
resp.validate();
- return resp.getTopic();
+ return new GenericTopic(resp.getTopic(), restClient, fullNamespace);
}
/**
@@ -194,7 +194,7 @@ public class MessagingCatalog extends BaseSchemaCatalog
implements TopicCatalog
ErrorHandlers.topicErrorHandler());
resp.validate();
- return resp.getTopic();
+ return new GenericTopic(resp.getTopic(), restClient, fullNamespace);
}
/**
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
new file mode 100644
index 000000000..a61e272ca
--- /dev/null
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectTagOperations.java
@@ -0,0 +1,119 @@
+/*
+ * 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.client;
+
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Locale;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.dto.requests.TagsAssociateRequest;
+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.rest.RESTUtils;
+import org.apache.gravitino.tag.SupportsTags;
+import org.apache.gravitino.tag.Tag;
+
+/**
+ * The implementation of {@link SupportsTags}. This interface will be
composited into catalog,
+ * schema, table, fileset and topic to provide tag operations for these
metadata objects
+ */
+class MetadataObjectTagOperations implements SupportsTags {
+
+ private final String metalakeName;
+
+ private final RESTClient restClient;
+
+ private final String tagRequestPath;
+
+ MetadataObjectTagOperations(
+ String metalakeName, MetadataObject metadataObject, RESTClient
restClient) {
+ this.metalakeName = metalakeName;
+ this.restClient = restClient;
+ this.tagRequestPath =
+ String.format(
+ "api/metalakes/%s/tags/%s/%s",
+ metalakeName,
+ metadataObject.type().name().toLowerCase(Locale.ROOT),
+ metadataObject.fullName());
+ }
+
+ @Override
+ public String[] listTags() {
+ NameListResponse resp =
+ restClient.get(
+ tagRequestPath,
+ NameListResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+
+ resp.validate();
+ return resp.getNames();
+ }
+
+ @Override
+ public Tag[] listTagsInfo() {
+ TagListResponse resp =
+ restClient.get(
+ tagRequestPath,
+ TagListResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+
+ resp.validate();
+ return Arrays.stream(resp.getTags())
+ .map(tagDTO -> new GenericTag(tagDTO, restClient, metalakeName))
+ .toArray(Tag[]::new);
+ }
+
+ @Override
+ public Tag getTag(String name) throws NoSuchTagException {
+ Preconditions.checkArgument(StringUtils.isNotBlank(name), "Tag name must
not be null or empty");
+
+ TagResponse resp =
+ restClient.get(
+ tagRequestPath + "/" + RESTUtils.encodeString(name),
+ TagResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+
+ resp.validate();
+ return new GenericTag(resp.getTag(), restClient, metalakeName);
+ }
+
+ @Override
+ public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove) {
+ TagsAssociateRequest request = new TagsAssociateRequest(tagsToAdd,
tagsToRemove);
+ request.validate();
+
+ NameListResponse resp =
+ restClient.post(
+ tagRequestPath,
+ request,
+ NameListResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.tagErrorHandler());
+
+ resp.validate();
+ return resp.getNames();
+ }
+}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
index c20752de0..4ae92f932 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
@@ -59,7 +59,7 @@ import org.apache.gravitino.rest.RESTUtils;
* operations, for example, schemas and tables list, creation, update and
deletion. A Relational
* catalog is under the metalake.
*/
-public class RelationalCatalog extends BaseSchemaCatalog implements
TableCatalog {
+class RelationalCatalog extends BaseSchemaCatalog implements TableCatalog {
RelationalCatalog(
Namespace namespace,
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java
index 4a147914f..af7e094b1 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java
@@ -22,12 +22,15 @@ import static
org.apache.gravitino.dto.util.DTOConverters.fromDTO;
import static org.apache.gravitino.dto.util.DTOConverters.toDTO;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
import lombok.SneakyThrows;
import org.apache.gravitino.Audit;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.dto.rel.TableDTO;
import org.apache.gravitino.dto.rel.partitions.PartitionDTO;
@@ -37,6 +40,7 @@ import
org.apache.gravitino.dto.responses.PartitionListResponse;
import org.apache.gravitino.dto.responses.PartitionNameListResponse;
import org.apache.gravitino.dto.responses.PartitionResponse;
import org.apache.gravitino.exceptions.NoSuchPartitionException;
+import org.apache.gravitino.exceptions.NoSuchTagException;
import org.apache.gravitino.exceptions.PartitionAlreadyExistsException;
import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.SupportsPartitions;
@@ -47,9 +51,21 @@ import
org.apache.gravitino.rel.expressions.transforms.Transform;
import org.apache.gravitino.rel.indexes.Index;
import org.apache.gravitino.rel.partitions.Partition;
import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.tag.SupportsTags;
+import org.apache.gravitino.tag.Tag;
/** Represents a relational table. */
-public class RelationalTable implements Table, SupportsPartitions {
+class RelationalTable implements Table, SupportsPartitions, SupportsTags {
+
+ private static final Joiner DOT_JOINER = Joiner.on(".");
+
+ private final Table table;
+
+ private final RESTClient restClient;
+
+ private final Namespace namespace;
+
+ private final MetadataObjectTagOperations objectTagOperations;
/**
* Creates a new RelationalTable.
@@ -59,15 +75,10 @@ public class RelationalTable implements Table,
SupportsPartitions {
* @param restClient The REST client.
* @return A new RelationalTable.
*/
- public static RelationalTable from(
- Namespace namespace, TableDTO tableDTO, RESTClient restClient) {
+ static RelationalTable from(Namespace namespace, TableDTO tableDTO,
RESTClient restClient) {
return new RelationalTable(namespace, tableDTO, restClient);
}
- private final Table table;
- private final RESTClient restClient;
- private final Namespace namespace;
-
/**
* Creates a new RelationalTable.
*
@@ -79,6 +90,10 @@ public class RelationalTable implements Table,
SupportsPartitions {
this.namespace = namespace;
this.restClient = restClient;
this.table = fromDTO(tableDTO);
+ MetadataObject tableObject =
+ MetadataObjects.parse(tableFullName(namespace, tableDTO.name()),
MetadataObject.Type.TABLE);
+ this.objectTagOperations =
+ new MetadataObjectTagOperations(namespace.level(0), tableObject,
restClient);
}
/**
@@ -154,7 +169,7 @@ public class RelationalTable implements Table,
SupportsPartitions {
/** @return The partition request path. */
@VisibleForTesting
- public String getPartitionRequestPath() {
+ String getPartitionRequestPath() {
return "api/metalakes/"
+ namespace.level(0)
+ "/catalogs/"
@@ -263,4 +278,33 @@ public class RelationalTable implements Table,
SupportsPartitions {
protected static String formatPartitionRequestPath(String prefix, String
partitionName) {
return prefix + "/" + RESTUtils.encodeString(partitionName);
}
+
+ @Override
+ public SupportsTags supportsTags() {
+ return this;
+ }
+
+ private static String tableFullName(Namespace tableNS, String tableName) {
+ return DOT_JOINER.join(tableNS.level(1), tableNS.level(2), tableName);
+ }
+
+ @Override
+ public String[] listTags() {
+ return objectTagOperations.listTags();
+ }
+
+ @Override
+ public Tag[] listTagsInfo() {
+ return objectTagOperations.listTagsInfo();
+ }
+
+ @Override
+ public Tag getTag(String name) throws NoSuchTagException {
+ return objectTagOperations.getTag(name);
+ }
+
+ @Override
+ public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove) {
+ return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+ }
}
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGenericTag.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGenericTag.java
new file mode 100644
index 000000000..0e86e9bf6
--- /dev/null
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGenericTag.java
@@ -0,0 +1,147 @@
+/*
+ * 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.client;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import java.time.Instant;
+import java.util.Collections;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.dto.MetalakeDTO;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.dto.responses.MetadataObjectListResponse;
+import org.apache.gravitino.dto.responses.MetalakeResponse;
+import org.apache.gravitino.dto.tag.MetadataObjectDTO;
+import org.apache.gravitino.dto.tag.TagDTO;
+import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.tag.Tag;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.Method;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class TestGenericTag extends TestBase {
+
+ private static String metalakeName = "metalake1";
+
+ private static TagDTO tagDTO =
+ TagDTO.builder()
+ .withName("tag1")
+ .withComment("comment1")
+ .withProperties(Collections.emptyMap())
+
.withAudit(AuditDTO.builder().withCreator("test").withCreateTime(Instant.now()).build())
+ .build();
+
+ private static GravitinoClient gravitinoClient;
+
+ @BeforeAll
+ public static void setUp() throws Exception {
+ TestBase.setUp();
+
+ MetalakeDTO mockMetalake =
+ MetalakeDTO.builder()
+ .withName(metalakeName)
+ .withComment("comment")
+ .withAudit(
+
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+ MetalakeResponse resp = new MetalakeResponse(mockMetalake);
+ buildMockResource(Method.GET, "/api/metalakes/" + metalakeName, null,
resp, HttpStatus.SC_OK);
+
+ gravitinoClient =
+ GravitinoClient.builder("http://127.0.0.1:" +
mockServer.getLocalPort())
+ .withMetalake(metalakeName)
+ .withVersionCheckDisabled()
+ .build();
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ TestBase.tearDown();
+ gravitinoClient.close();
+ }
+
+ @Test
+ public void testAssociatedObjects() throws JsonProcessingException {
+ Tag tag = new GenericTag(tagDTO, gravitinoClient.restClient(),
metalakeName);
+ String path = "/api/metalakes/" + metalakeName + "/tags/" + tagDTO.name()
+ "/objects";
+
+ MetadataObjectDTO[] objects =
+ new MetadataObjectDTO[] {
+ MetadataObjectDTO.builder()
+ .withParent(null)
+ .withName("catalog1")
+ .withType(MetadataObject.Type.CATALOG)
+ .build(),
+ MetadataObjectDTO.builder()
+ .withParent("catalog1")
+ .withName("schema1")
+ .withType(MetadataObject.Type.SCHEMA)
+ .build(),
+ MetadataObjectDTO.builder()
+ .withParent("catalog1.schema1")
+ .withName("table1")
+ .withType(MetadataObject.Type.TABLE)
+ .build()
+ };
+
+ MetadataObjectListResponse resp = new MetadataObjectListResponse(objects);
+ buildMockResource(Method.GET, path, null, resp, HttpStatus.SC_OK);
+
+ MetadataObject[] actualObjects = tag.associatedObjects().objects();
+ Assertions.assertEquals(objects.length, actualObjects.length);
+ for (int i = 0; i < objects.length; i++) {
+ MetadataObjectDTO object = objects[i];
+ MetadataObject actualObject = actualObjects[i];
+ Assertions.assertEquals(object.parent(), actualObject.parent());
+ Assertions.assertEquals(object.name(), actualObject.name());
+ Assertions.assertEquals(object.type(), actualObject.type());
+ }
+
+ // Test return empty array
+ buildMockResource(
+ Method.GET,
+ path,
+ null,
+ new MetadataObjectListResponse(new MetadataObjectDTO[0]),
+ HttpStatus.SC_OK);
+ MetadataObject[] actualObjects1 = tag.associatedObjects().objects();
+ Assertions.assertEquals(0, actualObjects1.length);
+
+ // Test throw NoSuchMetalakeException
+ ErrorResponse errorResponse =
+ ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(),
"mock error");
+ buildMockResource(Method.GET, path, null, errorResponse,
HttpStatus.SC_NOT_FOUND);
+
+ Throwable ex =
+ Assertions.assertThrows(
+ NoSuchMetalakeException.class, () ->
tag.associatedObjects().objects());
+ Assertions.assertEquals("mock error", ex.getMessage());
+
+ // Test throw internal error
+ ErrorResponse errorResponse1 = ErrorResponse.internalError("mock error");
+ buildMockResource(Method.GET, path, null, errorResponse1,
HttpStatus.SC_INTERNAL_SERVER_ERROR);
+
+ Throwable ex1 =
+ Assertions.assertThrows(RuntimeException.class, () ->
tag.associatedObjects().objects());
+ Assertions.assertEquals("mock error", ex1.getMessage());
+ }
+}
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoClient.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoAdminClient.java
similarity index 99%
rename from
clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoClient.java
rename to
clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoAdminClient.java
index 4c5cef167..66960791e 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoClient.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoAdminClient.java
@@ -54,7 +54,7 @@ import org.mockserver.matchers.Times;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
-public class TestGravitinoClient extends TestBase {
+public class TestGravitinoAdminClient extends TestBase {
@Test
public void testListMetalakes() throws JsonProcessingException {
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
index a3562cad6..48ade28d1 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGravitinoMetalake.java
@@ -35,18 +35,30 @@ import
org.apache.gravitino.dto.requests.CatalogCreateRequest;
import org.apache.gravitino.dto.requests.CatalogUpdateRequest;
import org.apache.gravitino.dto.requests.CatalogUpdatesRequest;
import org.apache.gravitino.dto.requests.MetalakeCreateRequest;
+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.responses.CatalogListResponse;
import org.apache.gravitino.dto.responses.CatalogResponse;
import org.apache.gravitino.dto.responses.DropResponse;
import org.apache.gravitino.dto.responses.EntityListResponse;
import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.dto.responses.MetalakeResponse;
+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.TagDTO;
import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchTagException;
import org.apache.gravitino.exceptions.RESTException;
+import org.apache.gravitino.exceptions.TagAlreadyExistsException;
+import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagChange;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Method;
+import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -81,6 +93,12 @@ public class TestGravitinoMetalake extends TestBase {
.build();
}
+ @AfterAll
+ public static void tearDown() {
+ TestBase.tearDown();
+ gravitinoClient.close();
+ }
+
@Test
public void testListCatalogs() throws JsonProcessingException {
String path = "/api/metalakes/" + metalakeName + "/catalogs";
@@ -390,6 +408,299 @@ public class TestGravitinoMetalake extends TestBase {
Assertions.assertFalse(dropped1, "catalog should be non-existent");
}
+ @Test
+ public void testListTags() throws JsonProcessingException {
+ String path = "/api/metalakes/" + metalakeName + "/tags";
+
+ String[] tagNames = new String[] {"tag1", "tag2"};
+ NameListResponse resp = new NameListResponse(tagNames);
+ buildMockResource(Method.GET, path, null, resp, HttpStatus.SC_OK);
+
+ String[] tags = gravitinoClient.listTags();
+ Assertions.assertEquals(2, tags.length);
+ Assertions.assertArrayEquals(tagNames, tags);
+
+ // Test return empty tag list
+ NameListResponse resp1 = new NameListResponse(new String[] {});
+ buildMockResource(Method.GET, path, null, resp1, HttpStatus.SC_OK);
+ String[] tags1 = gravitinoClient.listTags();
+ Assertions.assertEquals(0, tags1.length);
+
+ // Test throw MetalakeNotFoundException
+ ErrorResponse errorResponse =
+ ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(),
"mock error");
+ buildMockResource(Method.GET, path, null, errorResponse,
HttpStatus.SC_NOT_FOUND);
+ Throwable ex =
+ Assertions.assertThrows(NoSuchMetalakeException.class,
gravitinoClient::listTags);
+ Assertions.assertTrue(ex.getMessage().contains("mock error"));
+
+ // Test throw internal error
+ ErrorResponse errorResp = ErrorResponse.internalError("mock error");
+ buildMockResource(Method.GET, path, null, errorResp,
HttpStatus.SC_INTERNAL_SERVER_ERROR);
+ Throwable ex1 = Assertions.assertThrows(RuntimeException.class,
gravitinoClient::listTags);
+ Assertions.assertTrue(ex1.getMessage().contains("mock error"));
+ }
+
+ @Test
+ public void testListTagInfos() throws JsonProcessingException {
+ String path = "/api/metalakes/" + metalakeName + "/tags";
+ Map<String, String> params = Collections.singletonMap("details", "true");
+
+ TagDTO tag1 =
+ TagDTO.builder()
+ .withName("tag1")
+ .withComment("comment1")
+ .withAudit(
+
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+ TagDTO tag2 =
+ TagDTO.builder()
+ .withName("tag2")
+ .withComment("comment2")
+ .withAudit(
+
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+
+ TagDTO[] tags = new TagDTO[] {tag1, tag2};
+ TagListResponse resp = new TagListResponse(tags);
+ buildMockResource(Method.GET, path, params, null, resp, HttpStatus.SC_OK);
+
+ Tag[] tagInfos = gravitinoClient.listTagsInfo();
+ Assertions.assertEquals(2, tagInfos.length);
+ Assertions.assertEquals("tag1", tagInfos[0].name());
+ Assertions.assertEquals("comment1", tagInfos[0].comment());
+ Assertions.assertEquals("tag2", tagInfos[1].name());
+ Assertions.assertEquals("comment2", tagInfos[1].comment());
+
+ // Test empty tag list
+ TagListResponse resp1 = new TagListResponse(new TagDTO[] {});
+ buildMockResource(Method.GET, path, params, null, resp1, HttpStatus.SC_OK);
+ Tag[] tagInfos1 = gravitinoClient.listTagsInfo();
+ Assertions.assertEquals(0, tagInfos1.length);
+
+ // Test throw NoSuchMetalakeException
+ ErrorResponse errorResponse =
+ ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(),
"mock error");
+ buildMockResource(Method.GET, path, params, null, errorResponse,
HttpStatus.SC_NOT_FOUND);
+ Throwable ex =
+ Assertions.assertThrows(NoSuchMetalakeException.class,
gravitinoClient::listTagsInfo);
+ Assertions.assertTrue(ex.getMessage().contains("mock error"));
+
+ // Test throw internal error
+ ErrorResponse errorResp = ErrorResponse.internalError("mock error");
+ buildMockResource(
+ Method.GET, path, params, null, errorResp,
HttpStatus.SC_INTERNAL_SERVER_ERROR);
+ Throwable ex1 = Assertions.assertThrows(RuntimeException.class,
gravitinoClient::listTagsInfo);
+ Assertions.assertTrue(ex1.getMessage().contains("mock error"));
+ }
+
+ @Test
+ public void testGetTag() throws JsonProcessingException {
+ String tagName = "tag1";
+ String path = "/api/metalakes/" + metalakeName + "/tags/" + tagName;
+ TagDTO tag1 =
+ TagDTO.builder()
+ .withName(tagName)
+ .withComment("comment1")
+ .withAudit(
+
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+
+ TagResponse resp = new TagResponse(tag1);
+ buildMockResource(Method.GET, path, null, resp, HttpStatus.SC_OK);
+
+ Tag tag = gravitinoClient.getTag(tagName);
+ Assertions.assertEquals(tagName, tag.name());
+ Assertions.assertEquals("comment1", tag.comment());
+
+ // Test throw NoSuchMetalakeException
+ ErrorResponse errorResponse =
+ ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(),
"mock error");
+ buildMockResource(Method.GET, path, null, errorResponse,
HttpStatus.SC_NOT_FOUND);
+ Throwable ex =
+ Assertions.assertThrows(
+ NoSuchMetalakeException.class, () ->
gravitinoClient.getTag(tagName));
+ Assertions.assertTrue(ex.getMessage().contains("mock error"));
+
+ // Test throw NoSuchTagException
+ ErrorResponse errorResponse1 =
+ ErrorResponse.notFound(NoSuchTagException.class.getSimpleName(), "mock
error");
+ buildMockResource(Method.GET, path, null, errorResponse1,
HttpStatus.SC_NOT_FOUND);
+ Throwable ex1 =
+ Assertions.assertThrows(NoSuchTagException.class, () ->
gravitinoClient.getTag(tagName));
+ Assertions.assertTrue(ex1.getMessage().contains("mock error"));
+
+ // Test throw internal error
+ ErrorResponse errorResp = ErrorResponse.internalError("mock error");
+ buildMockResource(Method.GET, path, null, errorResp,
HttpStatus.SC_INTERNAL_SERVER_ERROR);
+ Throwable ex2 =
+ Assertions.assertThrows(RuntimeException.class, () ->
gravitinoClient.getTag(tagName));
+ Assertions.assertTrue(ex2.getMessage().contains("mock error"));
+ }
+
+ @Test
+ public void testCreateTag() throws JsonProcessingException {
+ String tagName = "tag1";
+ String path = "/api/metalakes/" + metalakeName + "/tags";
+
+ TagDTO tag1 =
+ TagDTO.builder()
+ .withName(tagName)
+ .withAudit(
+
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+ TagResponse resp = new TagResponse(tag1);
+
+ TagCreateRequest req = new TagCreateRequest(tagName, null, null);
+ buildMockResource(Method.POST, path, req, resp, HttpStatus.SC_OK);
+
+ Tag tag = gravitinoClient.createTag(tagName, null, null);
+ Assertions.assertEquals(tagName, tag.name());
+ Assertions.assertNull(tag.comment());
+ Assertions.assertNull(tag.properties());
+
+ // Test with null name
+ Throwable ex =
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
gravitinoClient.createTag(null, null, null));
+ Assertions.assertTrue(ex.getMessage().contains("tag name must not be null
or empty"));
+
+ // Test throw NoSuchMetalakeException
+ ErrorResponse errorResponse =
+ ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(),
"mock error");
+ buildMockResource(Method.POST, path, null, errorResponse,
HttpStatus.SC_NOT_FOUND);
+ Throwable ex1 =
+ Assertions.assertThrows(
+ NoSuchMetalakeException.class, () ->
gravitinoClient.createTag(tagName, null, null));
+ Assertions.assertTrue(ex1.getMessage().contains("mock error"));
+
+ // Test throw TagAlreadyExistsException
+ ErrorResponse errorResponse1 =
+
ErrorResponse.alreadyExists(TagAlreadyExistsException.class.getSimpleName(),
"mock error");
+ buildMockResource(Method.POST, path, null, errorResponse1,
HttpStatus.SC_CONFLICT);
+ Throwable ex2 =
+ Assertions.assertThrows(
+ TagAlreadyExistsException.class, () ->
gravitinoClient.createTag(tagName, null, null));
+ Assertions.assertTrue(ex2.getMessage().contains("mock error"));
+
+ // Test throw internal error
+ ErrorResponse errorResp = ErrorResponse.internalError("mock error");
+ buildMockResource(Method.POST, path, null, errorResp,
HttpStatus.SC_INTERNAL_SERVER_ERROR);
+ Throwable ex3 =
+ Assertions.assertThrows(
+ RuntimeException.class, () -> gravitinoClient.createTag(tagName,
null, null));
+ Assertions.assertTrue(ex3.getMessage().contains("mock error"));
+ }
+
+ @Test
+ public void testAlterTag() throws JsonProcessingException {
+ String tagName = "tag1";
+ String path = "/api/metalakes/" + metalakeName + "/tags/" + tagName;
+
+ TagDTO tag2 =
+ TagDTO.builder()
+ .withName("tag2")
+ .withComment("comment2")
+ .withAudit(
+
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+
+ TagResponse resp = new TagResponse(tag2);
+
+ List<TagUpdateRequest> reqs =
+ Arrays.asList(
+ new TagUpdateRequest.RenameTagRequest("tag2"),
+ new TagUpdateRequest.UpdateTagCommentRequest("comment2"));
+ TagUpdatesRequest request = new TagUpdatesRequest(reqs);
+ buildMockResource(Method.PUT, path, request, resp, HttpStatus.SC_OK);
+
+ Tag tag =
+ gravitinoClient.alterTag(
+ tagName, TagChange.rename("tag2"),
TagChange.updateComment("comment2"));
+ Assertions.assertEquals("tag2", tag.name());
+ Assertions.assertEquals("comment2", tag.comment());
+ Assertions.assertNull(tag.properties());
+
+ // Test throw NoSuchMetalakeException
+ ErrorResponse errorResponse =
+ ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(),
"mock error");
+ buildMockResource(Method.PUT, path, request, errorResponse,
HttpStatus.SC_NOT_FOUND);
+ Throwable ex =
+ Assertions.assertThrows(
+ NoSuchMetalakeException.class,
+ () ->
+ gravitinoClient.alterTag(
+ tagName, TagChange.rename("tag2"),
TagChange.updateComment("comment2")));
+ Assertions.assertTrue(ex.getMessage().contains("mock error"));
+
+ // Test throw NoSuchTagException
+ ErrorResponse errorResponse1 =
+ ErrorResponse.notFound(NoSuchTagException.class.getSimpleName(), "mock
error");
+ buildMockResource(Method.PUT, path, request, errorResponse1,
HttpStatus.SC_NOT_FOUND);
+ Throwable ex1 =
+ Assertions.assertThrows(
+ NoSuchTagException.class,
+ () ->
+ gravitinoClient.alterTag(
+ tagName, TagChange.rename("tag2"),
TagChange.updateComment("comment2")));
+ Assertions.assertTrue(ex1.getMessage().contains("mock error"));
+
+ // Test throw IllegalArgumentException
+ ErrorResponse errorResponse2 = ErrorResponse.illegalArguments("mock
error");
+ buildMockResource(Method.PUT, path, request, errorResponse2,
HttpStatus.SC_BAD_REQUEST);
+ Throwable ex2 =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ gravitinoClient.alterTag(
+ tagName, TagChange.rename("tag2"),
TagChange.updateComment("comment2")));
+ Assertions.assertTrue(ex2.getMessage().contains("mock error"));
+
+ // Test throw internal error
+ ErrorResponse errorResp = ErrorResponse.internalError("mock error");
+ buildMockResource(Method.PUT, path, request, errorResp,
HttpStatus.SC_INTERNAL_SERVER_ERROR);
+ Throwable ex3 =
+ Assertions.assertThrows(
+ RuntimeException.class,
+ () ->
+ gravitinoClient.alterTag(
+ tagName, TagChange.rename("tag2"),
TagChange.updateComment("comment2")));
+ Assertions.assertTrue(ex3.getMessage().contains("mock error"));
+ }
+
+ @Test
+ public void testDeleteTag() throws JsonProcessingException {
+ String tagName = "tag1";
+ String path = "/api/metalakes/" + metalakeName + "/tags/" + tagName;
+
+ DropResponse resp = new DropResponse(true);
+ buildMockResource(Method.DELETE, path, null, resp, HttpStatus.SC_OK);
+ boolean dropped = gravitinoClient.deleteTag(tagName);
+ Assertions.assertTrue(dropped);
+
+ // Test return false
+ DropResponse resp1 = new DropResponse(false);
+ buildMockResource(Method.DELETE, path, null, resp1, HttpStatus.SC_OK);
+ boolean dropped1 = gravitinoClient.deleteTag(tagName);
+ Assertions.assertFalse(dropped1);
+
+ // Test throw NoSuchMetalakeException
+ ErrorResponse errorResponse =
+ ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(),
"mock error");
+ buildMockResource(Method.DELETE, path, null, errorResponse,
HttpStatus.SC_NOT_FOUND);
+ Throwable ex =
+ Assertions.assertThrows(
+ NoSuchMetalakeException.class, () ->
gravitinoClient.deleteTag(tagName));
+ Assertions.assertTrue(ex.getMessage().contains("mock error"));
+
+ // Test internal error
+ ErrorResponse errorResp = ErrorResponse.internalError("mock error");
+ buildMockResource(Method.DELETE, path, null, errorResp,
HttpStatus.SC_INTERNAL_SERVER_ERROR);
+ Throwable ex1 =
+ Assertions.assertThrows(RuntimeException.class, () ->
gravitinoClient.deleteTag(tagName));
+ Assertions.assertTrue(ex1.getMessage().contains("mock error"));
+ }
+
static GravitinoMetalake createMetalake(GravitinoAdminClient client, String
metalakeName)
throws JsonProcessingException {
MetalakeDTO mockMetalake =
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
new file mode 100644
index 000000000..095d78549
--- /dev/null
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
@@ -0,0 +1,503 @@
+/*
+ * 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.client;
+
+import static org.apache.hc.core5.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+import static org.apache.hc.core5.http.HttpStatus.SC_NOT_FOUND;
+import static org.apache.hc.core5.http.HttpStatus.SC_OK;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import java.util.Collections;
+import java.util.Locale;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.dto.SchemaDTO;
+import org.apache.gravitino.dto.file.FilesetDTO;
+import org.apache.gravitino.dto.messaging.TopicDTO;
+import org.apache.gravitino.dto.rel.ColumnDTO;
+import org.apache.gravitino.dto.rel.TableDTO;
+import org.apache.gravitino.dto.requests.TagsAssociateRequest;
+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.dto.tag.TagDTO;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.exceptions.NotFoundException;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.messaging.Topic;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.tag.SupportsTags;
+import org.apache.gravitino.tag.Tag;
+import org.apache.hc.core5.http.Method;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class TestSupportTags extends TestBase {
+
+ private static final String METALAKE_NAME = "metalake";
+
+ private static Catalog relationalCatalog;
+
+ private static Catalog filesetCatalog;
+
+ private static Catalog messagingCatalog;
+
+ private static Schema genericSchema;
+
+ private static Table relationalTable;
+
+ private static Fileset genericFileset;
+
+ private static Topic genericTopic;
+
+ @BeforeAll
+ public static void setUp() throws Exception {
+ TestBase.setUp();
+ TestGravitinoMetalake.createMetalake(client, METALAKE_NAME);
+
+ relationalCatalog =
+ new RelationalCatalog(
+ Namespace.of(METALAKE_NAME),
+ "catalog1",
+ Catalog.Type.RELATIONAL,
+ "test",
+ "comment",
+ Collections.emptyMap(),
+ AuditDTO.builder().build(),
+ client.restClient());
+
+ filesetCatalog =
+ new FilesetCatalog(
+ Namespace.of(METALAKE_NAME),
+ "catalog2",
+ Catalog.Type.FILESET,
+ "test",
+ "comment",
+ Collections.emptyMap(),
+ AuditDTO.builder().build(),
+ client.restClient());
+
+ messagingCatalog =
+ new MessagingCatalog(
+ Namespace.of(METALAKE_NAME),
+ "catalog3",
+ Catalog.Type.MESSAGING,
+ "test",
+ "comment",
+ Collections.emptyMap(),
+ AuditDTO.builder().build(),
+ client.restClient());
+
+ genericSchema =
+ new GenericSchema(
+ SchemaDTO.builder()
+ .withName("schema1")
+ .withComment("comment1")
+ .withProperties(Collections.emptyMap())
+ .withAudit(AuditDTO.builder().withCreator("test").build())
+ .build(),
+ client.restClient(),
+ METALAKE_NAME,
+ "catalog1");
+
+ relationalTable =
+ RelationalTable.from(
+ Namespace.of(METALAKE_NAME, "catalog1", "schema1"),
+ TableDTO.builder()
+ .withName("table1")
+ .withComment("comment1")
+ .withColumns(
+ new ColumnDTO[] {
+ ColumnDTO.builder()
+ .withName("col1")
+ .withDataType(Types.IntegerType.get())
+ .build()
+ })
+ .withProperties(Collections.emptyMap())
+ .withAudit(AuditDTO.builder().withCreator("test").build())
+ .build(),
+ client.restClient());
+
+ genericFileset =
+ new GenericFileset(
+ FilesetDTO.builder()
+ .name("fileset1")
+ .comment("comment1")
+ .type(Fileset.Type.EXTERNAL)
+ .storageLocation("s3://bucket/path")
+ .properties(Collections.emptyMap())
+ .audit(AuditDTO.builder().withCreator("test").build())
+ .build(),
+ client.restClient(),
+ Namespace.of(METALAKE_NAME, "catalog1", "schema1"));
+
+ genericTopic =
+ new GenericTopic(
+ TopicDTO.builder()
+ .withName("topic1")
+ .withComment("comment1")
+ .withProperties(Collections.emptyMap())
+ .withAudit(AuditDTO.builder().withCreator("test").build())
+ .build(),
+ client.restClient(),
+ Namespace.of(METALAKE_NAME, "catalog1", "schema1"));
+ }
+
+ @Test
+ public void testListTagsForCatalog() throws JsonProcessingException {
+ testListTags(
+ relationalCatalog.supportsTags(),
+ MetadataObjects.of(null, relationalCatalog.name(),
MetadataObject.Type.CATALOG));
+
+ testListTags(
+ filesetCatalog.supportsTags(),
+ MetadataObjects.of(null, filesetCatalog.name(),
MetadataObject.Type.CATALOG));
+
+ testListTags(
+ messagingCatalog.supportsTags(),
+ MetadataObjects.of(null, messagingCatalog.name(),
MetadataObject.Type.CATALOG));
+ }
+
+ @Test
+ public void testListTagsForSchema() throws JsonProcessingException {
+ testListTags(
+ genericSchema.supportsTags(),
+ MetadataObjects.of("catalog1", genericSchema.name(),
MetadataObject.Type.SCHEMA));
+ }
+
+ @Test
+ public void testListTagsForTable() throws JsonProcessingException {
+ testListTags(
+ relationalTable.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", relationalTable.name(),
MetadataObject.Type.TABLE));
+ }
+
+ @Test
+ public void testListTagsForFileset() throws JsonProcessingException {
+ testListTags(
+ genericFileset.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", genericFileset.name(),
MetadataObject.Type.FILESET));
+ }
+
+ @Test
+ public void testListTagsForTopic() throws JsonProcessingException {
+ testListTags(
+ genericTopic.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", genericTopic.name(),
MetadataObject.Type.TOPIC));
+ }
+
+ @Test
+ public void testListTagsInfoForCatalog() throws JsonProcessingException {
+ testListTagsInfo(
+ relationalCatalog.supportsTags(),
+ MetadataObjects.of(null, relationalCatalog.name(),
MetadataObject.Type.CATALOG));
+
+ testListTagsInfo(
+ filesetCatalog.supportsTags(),
+ MetadataObjects.of(null, filesetCatalog.name(),
MetadataObject.Type.CATALOG));
+
+ testListTagsInfo(
+ messagingCatalog.supportsTags(),
+ MetadataObjects.of(null, messagingCatalog.name(),
MetadataObject.Type.CATALOG));
+ }
+
+ @Test
+ public void testListTagsInfoForSchema() throws JsonProcessingException {
+ testListTagsInfo(
+ genericSchema.supportsTags(),
+ MetadataObjects.of("catalog1", genericSchema.name(),
MetadataObject.Type.SCHEMA));
+ }
+
+ @Test
+ public void testListTagsInfoForTable() throws JsonProcessingException {
+ testListTagsInfo(
+ relationalTable.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", relationalTable.name(),
MetadataObject.Type.TABLE));
+ }
+
+ @Test
+ public void testListTagsInfoForFileset() throws JsonProcessingException {
+ testListTagsInfo(
+ genericFileset.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", genericFileset.name(),
MetadataObject.Type.FILESET));
+ }
+
+ @Test
+ public void testListTagsInfoForTopic() throws JsonProcessingException {
+ testListTagsInfo(
+ genericTopic.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", genericTopic.name(),
MetadataObject.Type.TOPIC));
+ }
+
+ @Test
+ public void testGetTagForCatalog() throws JsonProcessingException {
+ testGetTag(
+ relationalCatalog.supportsTags(),
+ MetadataObjects.of(null, relationalCatalog.name(),
MetadataObject.Type.CATALOG));
+
+ testGetTag(
+ filesetCatalog.supportsTags(),
+ MetadataObjects.of(null, filesetCatalog.name(),
MetadataObject.Type.CATALOG));
+
+ testGetTag(
+ messagingCatalog.supportsTags(),
+ MetadataObjects.of(null, messagingCatalog.name(),
MetadataObject.Type.CATALOG));
+ }
+
+ @Test
+ public void testGetTagForSchema() throws JsonProcessingException {
+ testGetTag(
+ genericSchema.supportsTags(),
+ MetadataObjects.of("catalog1", genericSchema.name(),
MetadataObject.Type.SCHEMA));
+ }
+
+ @Test
+ public void testGetTagForTable() throws JsonProcessingException {
+ testGetTag(
+ relationalTable.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", relationalTable.name(),
MetadataObject.Type.TABLE));
+ }
+
+ @Test
+ public void testGetTagForFileset() throws JsonProcessingException {
+ testGetTag(
+ genericFileset.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", genericFileset.name(),
MetadataObject.Type.FILESET));
+ }
+
+ @Test
+ public void testGetTagForTopic() throws JsonProcessingException {
+ testGetTag(
+ genericTopic.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", genericTopic.name(),
MetadataObject.Type.TOPIC));
+ }
+
+ @Test
+ public void testAssociateTagsForCatalog() throws JsonProcessingException {
+ testAssociateTags(
+ relationalCatalog.supportsTags(),
+ MetadataObjects.of(null, relationalCatalog.name(),
MetadataObject.Type.CATALOG));
+
+ testAssociateTags(
+ filesetCatalog.supportsTags(),
+ MetadataObjects.of(null, filesetCatalog.name(),
MetadataObject.Type.CATALOG));
+
+ testAssociateTags(
+ messagingCatalog.supportsTags(),
+ MetadataObjects.of(null, messagingCatalog.name(),
MetadataObject.Type.CATALOG));
+ }
+
+ @Test
+ public void testAssociateTagsForSchema() throws JsonProcessingException {
+ testAssociateTags(
+ genericSchema.supportsTags(),
+ MetadataObjects.of("catalog1", genericSchema.name(),
MetadataObject.Type.SCHEMA));
+ }
+
+ @Test
+ public void testAssociateTagsForTable() throws JsonProcessingException {
+ testAssociateTags(
+ relationalTable.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", relationalTable.name(),
MetadataObject.Type.TABLE));
+ }
+
+ @Test
+ public void testAssociateTagsForFileset() throws JsonProcessingException {
+ testAssociateTags(
+ genericFileset.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", genericFileset.name(),
MetadataObject.Type.FILESET));
+ }
+
+ @Test
+ public void testAssociateTagsForTopic() throws JsonProcessingException {
+ testAssociateTags(
+ genericTopic.supportsTags(),
+ MetadataObjects.of("catalog1.schema1", genericTopic.name(),
MetadataObject.Type.TOPIC));
+ }
+
+ private void testListTags(SupportsTags supportsTags, MetadataObject
metadataObject)
+ throws JsonProcessingException {
+ String path =
+ "/api/metalakes/"
+ + METALAKE_NAME
+ + "/tags/"
+ + metadataObject.type().name().toLowerCase(Locale.ROOT)
+ + "/"
+ + metadataObject.fullName();
+
+ String[] tags = new String[] {"tag1", "tag2"};
+ NameListResponse resp = new NameListResponse(tags);
+ buildMockResource(Method.GET, path, null, resp, SC_OK);
+
+ String[] actualTags = supportsTags.listTags();
+ Assertions.assertArrayEquals(tags, actualTags);
+
+ // Return empty list
+ NameListResponse resp1 = new NameListResponse(new String[0]);
+ buildMockResource(Method.GET, path, null, resp1, SC_OK);
+
+ String[] actualTags1 = supportsTags.listTags();
+ Assertions.assertArrayEquals(new String[0], actualTags1);
+
+ // Test throw NotFoundException
+ ErrorResponse errorResp =
+ ErrorResponse.notFound(NotFoundException.class.getSimpleName(), "mock
error");
+ buildMockResource(Method.GET, path, null, errorResp, SC_NOT_FOUND);
+
+ Throwable ex = Assertions.assertThrows(NotFoundException.class,
supportsTags::listTags);
+ Assertions.assertTrue(ex.getMessage().contains("mock error"));
+
+ // Test throw internal error
+ ErrorResponse errorResp1 = ErrorResponse.internalError("mock error");
+ buildMockResource(Method.GET, path, null, errorResp1,
SC_INTERNAL_SERVER_ERROR);
+
+ Throwable ex1 = Assertions.assertThrows(RuntimeException.class,
supportsTags::listTags);
+ Assertions.assertTrue(ex1.getMessage().contains("mock error"));
+ }
+
+ private void testListTagsInfo(SupportsTags supportsTags, MetadataObject
metadataObject)
+ throws JsonProcessingException {
+ String path =
+ "/api/metalakes/"
+ + METALAKE_NAME
+ + "/tags/"
+ + metadataObject.type().name().toLowerCase(Locale.ROOT)
+ + "/"
+ + metadataObject.fullName();
+
+ TagDTO tag1 =
+ TagDTO.builder()
+ .withName("tag1")
+ .withAudit(AuditDTO.builder().withCreator("test").build())
+ .build();
+ TagDTO tag2 =
+ TagDTO.builder()
+ .withName("tag2")
+ .withAudit(AuditDTO.builder().withCreator("test").build())
+ .build();
+ TagDTO[] tags = new TagDTO[] {tag1, tag2};
+ TagListResponse resp = new TagListResponse(tags);
+ buildMockResource(Method.GET, path, null, resp, SC_OK);
+
+ Tag[] actualTags = supportsTags.listTagsInfo();
+ Assertions.assertEquals(2, actualTags.length);
+ Assertions.assertEquals("tag1", actualTags[0].name());
+ Assertions.assertEquals("tag2", actualTags[1].name());
+
+ // Return empty list
+ TagListResponse resp1 = new TagListResponse(new TagDTO[0]);
+ buildMockResource(Method.GET, path, null, resp1, SC_OK);
+
+ Tag[] actualTags1 = supportsTags.listTagsInfo();
+ Assertions.assertArrayEquals(new Tag[0], actualTags1);
+
+ // Test throw NotFoundException
+ ErrorResponse errorResp =
+ ErrorResponse.notFound(NotFoundException.class.getSimpleName(), "mock
error");
+ buildMockResource(Method.GET, path, null, errorResp, SC_NOT_FOUND);
+
+ Throwable ex = Assertions.assertThrows(NotFoundException.class,
supportsTags::listTags);
+ Assertions.assertTrue(ex.getMessage().contains("mock error"));
+
+ // Test throw internal error
+ ErrorResponse errorResp1 = ErrorResponse.internalError("mock error");
+ buildMockResource(Method.GET, path, null, errorResp1,
SC_INTERNAL_SERVER_ERROR);
+
+ Throwable ex1 = Assertions.assertThrows(RuntimeException.class,
supportsTags::listTags);
+ Assertions.assertTrue(ex1.getMessage().contains("mock error"));
+ }
+
+ private void testGetTag(SupportsTags supportsTags, MetadataObject
metadataObject)
+ throws JsonProcessingException {
+ String path =
+ "/api/metalakes/"
+ + METALAKE_NAME
+ + "/tags/"
+ + metadataObject.type().name().toLowerCase(Locale.ROOT)
+ + "/"
+ + metadataObject.fullName()
+ + "/tag1";
+
+ TagDTO tag1 =
+ TagDTO.builder()
+ .withName("tag1")
+ .withAudit(AuditDTO.builder().withCreator("test").build())
+ .build();
+
+ TagResponse resp = new TagResponse(tag1);
+ buildMockResource(Method.GET, path, null, resp, SC_OK);
+
+ Tag actualTag = supportsTags.getTag("tag1");
+ Assertions.assertEquals("tag1", actualTag.name());
+
+ // Test throw NoSuchTagException
+ ErrorResponse errorResp =
+ ErrorResponse.notFound(NoSuchTagException.class.getSimpleName(), "mock
error");
+ buildMockResource(Method.GET, path, null, errorResp, SC_NOT_FOUND);
+
+ Throwable ex =
+ Assertions.assertThrows(NoSuchTagException.class, () ->
supportsTags.getTag("tag1"));
+ Assertions.assertTrue(ex.getMessage().contains("mock error"));
+
+ // Test throw internal error
+ ErrorResponse errorResp1 = ErrorResponse.internalError("mock error");
+ buildMockResource(Method.GET, path, null, errorResp1,
SC_INTERNAL_SERVER_ERROR);
+
+ Throwable ex1 =
+ Assertions.assertThrows(RuntimeException.class, () ->
supportsTags.getTag("tag1"));
+ Assertions.assertTrue(ex1.getMessage().contains("mock error"));
+ }
+
+ private void testAssociateTags(SupportsTags supportsTags, MetadataObject
metadataObject)
+ throws JsonProcessingException {
+ String path =
+ "/api/metalakes/"
+ + METALAKE_NAME
+ + "/tags/"
+ + metadataObject.type().name().toLowerCase(Locale.ROOT)
+ + "/"
+ + metadataObject.fullName();
+
+ String[] tagsToAdd = new String[] {"tag1", "tag2"};
+ String[] tagsToRemove = new String[] {"tag3", "tag4"};
+ TagsAssociateRequest request = new TagsAssociateRequest(tagsToAdd,
tagsToRemove);
+
+ NameListResponse resp = new NameListResponse(tagsToAdd);
+ buildMockResource(Method.POST, path, request, resp, SC_OK);
+
+ String[] actualTags = supportsTags.associateTags(tagsToAdd, tagsToRemove);
+ Assertions.assertArrayEquals(tagsToAdd, actualTags);
+
+ // Test throw internal error
+ ErrorResponse errorResp1 = ErrorResponse.internalError("mock error");
+ buildMockResource(Method.POST, path, request, errorResp1,
SC_INTERNAL_SERVER_ERROR);
+
+ Throwable ex1 =
+ Assertions.assertThrows(
+ RuntimeException.class, () ->
supportsTags.associateTags(tagsToAdd, tagsToRemove));
+ Assertions.assertTrue(ex1.getMessage().contains("mock error"));
+ }
+}
diff --git a/common/src/main/java/org/apache/gravitino/dto/SchemaDTO.java
b/common/src/main/java/org/apache/gravitino/dto/SchemaDTO.java
index 5ba9a58de..90ff2e16d 100644
--- a/common/src/main/java/org/apache/gravitino/dto/SchemaDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/SchemaDTO.java
@@ -21,9 +21,11 @@ package org.apache.gravitino.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import java.util.Map;
+import lombok.EqualsAndHashCode;
import org.apache.gravitino.Schema;
/** Represents a Schema DTO (Data Transfer Object). */
+@EqualsAndHashCode
public class SchemaDTO implements Schema {
@JsonProperty("name")
diff --git a/common/src/main/java/org/apache/gravitino/dto/file/FilesetDTO.java
b/common/src/main/java/org/apache/gravitino/dto/file/FilesetDTO.java
index 3231404d6..7630b4c8d 100644
--- a/common/src/main/java/org/apache/gravitino/dto/file/FilesetDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/file/FilesetDTO.java
@@ -25,6 +25,7 @@ import javax.annotation.Nullable;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
+import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.dto.AuditDTO;
@@ -33,6 +34,7 @@ import org.apache.gravitino.file.Fileset;
/** Represents a Fileset DTO (Data Transfer Object). */
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@EqualsAndHashCode
public class FilesetDTO implements Fileset {
@JsonProperty("name")
diff --git a/common/src/main/java/org/apache/gravitino/dto/tag/TagDTO.java
b/common/src/main/java/org/apache/gravitino/dto/tag/TagDTO.java
index a7f25480b..83c1573f7 100644
--- a/common/src/main/java/org/apache/gravitino/dto/tag/TagDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/tag/TagDTO.java
@@ -19,14 +19,13 @@
package org.apache.gravitino.dto.tag;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Objects;
import java.util.Map;
import java.util.Optional;
-import lombok.EqualsAndHashCode;
import org.apache.gravitino.dto.AuditDTO;
import org.apache.gravitino.tag.Tag;
/** Represents a Tag Data Transfer Object (DTO). */
-@EqualsAndHashCode
public class TagDTO implements Tag {
@JsonProperty("name")
@@ -71,6 +70,27 @@ public class TagDTO implements Tag {
return inherited;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof TagDTO)) {
+ return false;
+ }
+
+ TagDTO tagDTO = (TagDTO) o;
+ return Objects.equal(name, tagDTO.name)
+ && Objects.equal(comment, tagDTO.comment)
+ && Objects.equal(properties, tagDTO.properties)
+ && Objects.equal(audit, tagDTO.audit);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(name, comment, properties, audit);
+ }
+
/** @return a new builder for constructing a Tag DTO. */
public static Builder builder() {
return new Builder();