This is an automated email from the ASF dual-hosted git repository.
jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 71e6651e2 [#3914] feat(server): Add REST server interface for Tag
System (#3943)
71e6651e2 is described below
commit 71e6651e2bc7359a9c9b694fa7275871e4e11969
Author: Jerry Shao <[email protected]>
AuthorDate: Mon Jul 22 20:48:52 2024 +0800
[#3914] feat(server): Add REST server interface for Tag System (#3943)
### What changes were proposed in this pull request?
This PR proposes to add REST server interface for Tag System
### Why are the changes needed?
This is a part of work for Tag system.
Fix: #3914
### Does this PR introduce _any_ user-facing change?
Yes
### How was this patch tested?
UTs added.
---------
Co-authored-by: bknbkn <[email protected]>
Co-authored-by: Dev Parikh <[email protected]>
Co-authored-by: roryqi <[email protected]>
Co-authored-by: JinsYin <[email protected]>
Co-authored-by: rqyin <[email protected]>
---
.../java/org/apache/gravitino/MetadataObjects.java | 41 +
.../gravitino/client/ObjectMapperProvider.java | 4 +-
.../gravitino/dto/requests/TagCreateRequest.java | 76 ++
.../gravitino/dto/requests/TagUpdateRequest.java | 201 ++++
.../gravitino/dto/requests/TagUpdatesRequest.java | 57 +
.../dto/requests/TagsAssociateRequest.java | 82 ++
.../dto/responses/MetadataObjectListResponse.java | 73 ++
.../gravitino/dto/responses/NameListResponse.java | 65 ++
.../gravitino/dto/responses/TagListResponse.java | 65 ++
.../gravitino/dto/responses/TagResponse.java | 62 ++
.../gravitino/dto/tag/MetadataObjectDTO.java | 124 +++
.../java/org/apache/gravitino/dto/tag/TagDTO.java | 147 +++
.../apache/gravitino/dto/util/DTOConverters.java | 38 +
.../java/org/apache/gravitino/json/JsonUtils.java | 7 +-
.../dto/requests/TestTagCreateRequest.java | 49 +
.../dto/requests/TestTagUpdatesRequest.java | 91 ++
.../gravitino/dto/responses/TestResponses.java | 57 +
.../gravitino/dto/tag/TestMetadataObjectDTO.java | 150 +++
.../org/apache/gravitino/dto/tag/TestTagDTO.java | 103 ++
.../gravitino/tag/SupportsTagOperations.java | 1 -
.../java/org/apache/gravitino/tag/TagManager.java | 7 +-
.../apache/gravitino/server/GravitinoServer.java | 3 +-
.../gravitino/server/web/ObjectMapperProvider.java | 4 +-
.../server/web/rest/ExceptionHandlers.java | 42 +
.../gravitino/server/web/rest/OperationType.java | 3 +-
.../gravitino/server/web/rest/TagOperations.java | 451 ++++++++
.../server/web/rest/TestTagOperations.java | 1099 ++++++++++++++++++++
27 files changed, 3093 insertions(+), 9 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/MetadataObjects.java
b/api/src/main/java/org/apache/gravitino/MetadataObjects.java
index 5136164c9..6bd72137e 100644
--- a/api/src/main/java/org/apache/gravitino/MetadataObjects.java
+++ b/api/src/main/java/org/apache/gravitino/MetadataObjects.java
@@ -22,6 +22,7 @@ import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import java.util.List;
+import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
/** The helper class for {@link MetadataObject}. */
@@ -94,6 +95,46 @@ public class MetadataObjects {
return new MetadataObjectImpl(getParentFullName(names),
getLastName(names), type);
}
+ /**
+ * Get the parent metadata object of the given metadata object.
+ *
+ * @param object The metadata object
+ * @return The parent metadata object if it exists, otherwise null
+ */
+ @Nullable
+ public static MetadataObject parent(MetadataObject object) {
+ if (object == null) {
+ return null;
+ }
+
+ // Return null if the object is the root object
+ if (object.type() == MetadataObject.Type.METALAKE
+ || object.type() == MetadataObject.Type.CATALOG) {
+ return null;
+ }
+
+ MetadataObject.Type parentType;
+ switch (object.type()) {
+ case COLUMN:
+ parentType = MetadataObject.Type.TABLE;
+ break;
+ case TABLE:
+ case FILESET:
+ case TOPIC:
+ parentType = MetadataObject.Type.SCHEMA;
+ break;
+ case SCHEMA:
+ parentType = MetadataObject.Type.CATALOG;
+ break;
+
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected to reach here for metadata object type: " +
object.type());
+ }
+
+ return parse(object.parent(), parentType);
+ }
+
/**
* Parse the metadata object with the given full name and type.
*
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/ObjectMapperProvider.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/ObjectMapperProvider.java
index 1a906c581..44aab1b63 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/ObjectMapperProvider.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/ObjectMapperProvider.java
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.EnumFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
/**
@@ -39,7 +40,8 @@ public class ObjectMapperProvider {
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build()
- .registerModule(new JavaTimeModule());
+ .registerModule(new JavaTimeModule())
+ .registerModule(new Jdk8Module());
}
/**
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/TagCreateRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/TagCreateRequest.java
new file mode 100644
index 000000000..5b786f47c
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/TagCreateRequest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.dto.requests;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import javax.annotation.Nullable;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to create a tag. */
+@Getter
+@EqualsAndHashCode
+@ToString
+public class TagCreateRequest implements RESTRequest {
+
+ @JsonProperty("name")
+ private final String name;
+
+ @JsonProperty("comment")
+ @Nullable
+ private final String comment;
+
+ @JsonProperty("properties")
+ @Nullable
+ private Map<String, String> properties;
+
+ /**
+ * Creates a new TagCreateRequest.
+ *
+ * @param name The name of the tag.
+ * @param comment The comment of the tag.
+ * @param properties The properties of the tag.
+ */
+ public TagCreateRequest(String name, String comment, Map<String, String>
properties) {
+ this.name = name;
+ this.comment = comment;
+ this.properties = properties;
+ }
+
+ /** This is the constructor that is used by Jackson deserializer */
+ public TagCreateRequest() {
+ this(null, null, null);
+ }
+
+ /**
+ * Validates the request.
+ *
+ * @throws IllegalArgumentException If the request is invalid, this
exception is thrown.
+ */
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(name), "\"name\" is required and cannot be
empty");
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdateRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdateRequest.java
new file mode 100644
index 000000000..5323ac565
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdateRequest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.dto.requests;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.google.common.base.Preconditions;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.rest.RESTRequest;
+import org.apache.gravitino.tag.TagChange;
+
+/** Represents a request to update a tag. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = TagUpdateRequest.RenameTagRequest.class, name =
"rename"),
+ @JsonSubTypes.Type(
+ value = TagUpdateRequest.UpdateTagCommentRequest.class,
+ name = "updateComment"),
+ @JsonSubTypes.Type(value = TagUpdateRequest.SetTagPropertyRequest.class,
name = "setProperty"),
+ @JsonSubTypes.Type(
+ value = TagUpdateRequest.RemoveTagPropertyRequest.class,
+ name = "removeProperty")
+})
+public interface TagUpdateRequest extends RESTRequest {
+
+ /**
+ * Returns the tag change.
+ *
+ * @return the tag change.
+ */
+ TagChange tagChange();
+
+ /** The tag update request for renaming a tag. */
+ @EqualsAndHashCode
+ @ToString
+ class RenameTagRequest implements TagUpdateRequest {
+
+ @Getter
+ @JsonProperty("newName")
+ private final String newName;
+
+ /**
+ * Creates a new RenameTagRequest.
+ *
+ * @param newName The new name of the tag.
+ */
+ public RenameTagRequest(String newName) {
+ this.newName = newName;
+ }
+
+ /** This is the constructor that is used by Jackson deserializer */
+ public RenameTagRequest() {
+ this.newName = null;
+ }
+
+ @Override
+ public TagChange tagChange() {
+ return TagChange.rename(newName);
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(StringUtils.isNotBlank(newName),
"\"newName\" must not be blank");
+ }
+ }
+
+ /** The tag update request for updating a tag comment. */
+ @EqualsAndHashCode
+ @ToString
+ class UpdateTagCommentRequest implements TagUpdateRequest {
+
+ @Getter
+ @JsonProperty("newComment")
+ private final String newComment;
+
+ /**
+ * Creates a new UpdateTagCommentRequest.
+ *
+ * @param newComment The new comment of the tag.
+ */
+ public UpdateTagCommentRequest(String newComment) {
+ this.newComment = newComment;
+ }
+
+ /** This is the constructor that is used by Jackson deserializer */
+ public UpdateTagCommentRequest() {
+ this.newComment = null;
+ }
+
+ @Override
+ public TagChange tagChange() {
+ return TagChange.updateComment(newComment);
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(newComment), "\"newComment\" must not be
blank");
+ }
+ }
+
+ /** The tag update request for setting a tag property. */
+ @EqualsAndHashCode
+ @ToString
+ class SetTagPropertyRequest implements TagUpdateRequest {
+
+ @Getter
+ @JsonProperty("property")
+ private final String property;
+
+ @Getter
+ @JsonProperty("value")
+ private final String value;
+
+ /**
+ * Creates a new SetTagPropertyRequest.
+ *
+ * @param property The property to set.
+ * @param value The value of the property.
+ */
+ public SetTagPropertyRequest(String property, String value) {
+ this.property = property;
+ this.value = value;
+ }
+
+ /** This is the constructor that is used by Jackson deserializer */
+ public SetTagPropertyRequest() {
+ this.property = null;
+ this.value = null;
+ }
+
+ @Override
+ public TagChange tagChange() {
+ return TagChange.setProperty(property, value);
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(property), "\"property\" must not be blank");
+ Preconditions.checkArgument(StringUtils.isNotBlank(value), "\"value\"
must not be blank");
+ }
+ }
+
+ /** The tag update request for removing a tag property. */
+ @EqualsAndHashCode
+ @ToString
+ class RemoveTagPropertyRequest implements TagUpdateRequest {
+
+ @Getter
+ @JsonProperty("property")
+ private final String property;
+
+ /**
+ * Creates a new RemoveTagPropertyRequest.
+ *
+ * @param property The property to remove.
+ */
+ public RemoveTagPropertyRequest(String property) {
+ this.property = property;
+ }
+
+ /** This is the constructor that is used by Jackson deserializer */
+ public RemoveTagPropertyRequest() {
+ this.property = null;
+ }
+
+ @Override
+ public TagChange tagChange() {
+ return TagChange.removeProperty(property);
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(property), "\"property\" must not be blank");
+ }
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdatesRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdatesRequest.java
new file mode 100644
index 000000000..4e878b0f7
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/TagUpdatesRequest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.dto.requests;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.List;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to update a tag. */
+@Getter
+@EqualsAndHashCode
+@ToString
+public class TagUpdatesRequest implements RESTRequest {
+
+ @JsonProperty("updates")
+ private final List<TagUpdateRequest> updates;
+
+ /**
+ * Creates a new TagUpdatesRequest.
+ *
+ * @param updates The updates to apply to the tag.
+ */
+ public TagUpdatesRequest(List<TagUpdateRequest> updates) {
+ this.updates = updates;
+ }
+
+ /** This is the constructor that is used by Jackson deserializer */
+ public TagUpdatesRequest() {
+ this(null);
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(updates != null, "updates must not be null");
+ updates.forEach(RESTRequest::validate);
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/TagsAssociateRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/TagsAssociateRequest.java
new file mode 100644
index 000000000..fe202b1d6
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/TagsAssociateRequest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.dto.requests;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to associate tags. */
+@Getter
+@EqualsAndHashCode
+@ToString
+public class TagsAssociateRequest implements RESTRequest {
+
+ @JsonProperty("tagsToAdd")
+ private final String[] tagsToAdd;
+
+ @JsonProperty("tagsToRemove")
+ private final String[] tagsToRemove;
+
+ /**
+ * Creates a new TagsAssociateRequest.
+ *
+ * @param tagsToAdd The tags to add.
+ * @param tagsToRemove The tags to remove.
+ */
+ public TagsAssociateRequest(String[] tagsToAdd, String[] tagsToRemove) {
+ this.tagsToAdd = tagsToAdd;
+ this.tagsToRemove = tagsToRemove;
+ }
+
+ /** This is the constructor that is used by Jackson deserializer */
+ public TagsAssociateRequest() {
+ this(null, null);
+ }
+
+ /**
+ * Validates the request.
+ *
+ * @throws IllegalArgumentException If the request is invalid, this
exception is thrown.
+ */
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ tagsToAdd != null || tagsToRemove != null,
+ "tagsToAdd and tagsToRemove cannot both be null");
+
+ if (tagsToAdd != null) {
+ for (String tag : tagsToAdd) {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(tag), "tagsToAdd must not contain null or
empty tag names");
+ }
+ }
+
+ if (tagsToRemove != null) {
+ for (String tag : tagsToRemove) {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(tag), "tagsToRemove must not contain null
or empty tag names");
+ }
+ }
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/MetadataObjectListResponse.java
b/common/src/main/java/org/apache/gravitino/dto/responses/MetadataObjectListResponse.java
new file mode 100644
index 000000000..67b26a5f5
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/responses/MetadataObjectListResponse.java
@@ -0,0 +1,73 @@
+/*
+ * 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.dto.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.dto.tag.MetadataObjectDTO;
+
+/** Represents a response containing a list of metadata objects. */
+@Getter
+@EqualsAndHashCode(callSuper = true)
+@ToString
+public class MetadataObjectListResponse extends BaseResponse {
+
+ @JsonProperty("metadataObjects")
+ private final MetadataObjectDTO[] metadataObjects;
+
+ /**
+ * Constructor for MetadataObjectListResponse.
+ *
+ * @param metadataObjects The array of metadata object DTOs.
+ */
+ public MetadataObjectListResponse(MetadataObjectDTO[] metadataObjects) {
+ super(0);
+ this.metadataObjects = metadataObjects;
+ }
+
+ /** Default constructor for MetadataObjectListResponse. (Used for Jackson
deserialization.) */
+ public MetadataObjectListResponse() {
+ super();
+ this.metadataObjects = null;
+ }
+
+ /**
+ * Validates the response data.
+ *
+ * @throws IllegalArgumentException if name or audit information is not set.
+ */
+ public void validate() throws IllegalArgumentException {
+ super.validate();
+
+ Preconditions.checkArgument(metadataObjects != null, "metadataObjects must
be non-null");
+ Arrays.stream(metadataObjects)
+ .forEach(
+ object ->
+ Preconditions.checkArgument(
+ object != null
+ && StringUtils.isNotBlank(object.name())
+ && object.type() != null,
+ "metadataObject must not be null and it's field cannot
null or empty"));
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/NameListResponse.java
b/common/src/main/java/org/apache/gravitino/dto/responses/NameListResponse.java
new file mode 100644
index 000000000..0728165c5
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/responses/NameListResponse.java
@@ -0,0 +1,65 @@
+/*
+ * 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.dto.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+
+/** Represents a response for a list of entity names. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class NameListResponse extends BaseResponse {
+
+ @JsonProperty("names")
+ private final String[] names;
+
+ /**
+ * Creates a new NameListResponse.
+ *
+ * @param names The list of names.
+ */
+ public NameListResponse(String[] names) {
+ this.names = names;
+ }
+
+ /**
+ * This is the constructor that is used by Jackson deserializer to create an
instance of
+ * NameListResponse.
+ */
+ public NameListResponse() {
+ this.names = null;
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ super.validate();
+
+ Preconditions.checkArgument(names != null, "\"names\" must not be null");
+ Arrays.stream(names)
+ .forEach(
+ name ->
+ Preconditions.checkArgument(StringUtils.isNotBlank(name),
"name must not be null"));
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/TagListResponse.java
b/common/src/main/java/org/apache/gravitino/dto/responses/TagListResponse.java
new file mode 100644
index 000000000..93c324dfe
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/responses/TagListResponse.java
@@ -0,0 +1,65 @@
+/*
+ * 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.dto.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.gravitino.dto.tag.TagDTO;
+
+/** Represents a response for a list of tags. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class TagListResponse extends BaseResponse {
+
+ @JsonProperty("tags")
+ private final TagDTO[] tags;
+
+ /**
+ * Creates a new TagListResponse.
+ *
+ * @param tags The list of tags.
+ */
+ public TagListResponse(TagDTO[] tags) {
+ super(0);
+ this.tags = tags;
+ }
+
+ /**
+ * This is the constructor that is used by Jackson deserializer to create an
instance of
+ * TagListResponse.
+ */
+ public TagListResponse() {
+ super();
+ this.tags = null;
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ super.validate();
+
+ Preconditions.checkArgument(tags != null, "\"tags\" must not be null");
+ Arrays.stream(tags)
+ .forEach(t -> Preconditions.checkArgument(t != null, "tag must not be
null"));
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/TagResponse.java
b/common/src/main/java/org/apache/gravitino/dto/responses/TagResponse.java
new file mode 100644
index 000000000..9f154e4ed
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/responses/TagResponse.java
@@ -0,0 +1,62 @@
+/*
+ * 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.dto.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.gravitino.dto.tag.TagDTO;
+
+/** Represents a response for a tag. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class TagResponse extends BaseResponse {
+
+ @JsonProperty("tag")
+ private final TagDTO tag;
+
+ /**
+ * Creates a new TagResponse.
+ *
+ * @param tag The tag.
+ */
+ public TagResponse(TagDTO tag) {
+ super(0);
+ this.tag = tag;
+ }
+
+ /**
+ * This is the constructor that is used by Jackson deserializer to create an
instance of
+ * TagResponse.
+ */
+ public TagResponse() {
+ super();
+ this.tag = null;
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ super.validate();
+
+ Preconditions.checkArgument(tag != null, "\"tag\" must not be null");
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/tag/MetadataObjectDTO.java
b/common/src/main/java/org/apache/gravitino/dto/tag/MetadataObjectDTO.java
new file mode 100644
index 000000000..d09815b9e
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/tag/MetadataObjectDTO.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.dto.tag;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.EqualsAndHashCode;
+import org.apache.gravitino.MetadataObject;
+
+/** Represents a Metadata Object DTO (Data Transfer Object). */
+@EqualsAndHashCode
+public class MetadataObjectDTO implements MetadataObject {
+
+ private String parent;
+
+ private String name;
+
+ @JsonProperty("type")
+ private Type type;
+
+ private MetadataObjectDTO() {}
+
+ @Override
+ public String parent() {
+ return parent;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public Type type() {
+ return type;
+ }
+
+ /** @return The full name of the metadata object. */
+ @JsonProperty("fullName")
+ public String getFullName() {
+ return fullName();
+ }
+
+ /**
+ * Sets the full name of the metadata object. Only used by Jackson
deserializer.
+ *
+ * @param fullName The full name of the metadata object.
+ */
+ @JsonProperty("fullName")
+ public void setFullName(String fullName) {
+ int index = fullName.lastIndexOf(".");
+ if (index == -1) {
+ parent = null;
+ name = fullName;
+ } else {
+ parent = fullName.substring(0, index);
+ name = fullName.substring(index + 1);
+ }
+ }
+
+ /** @return a new builder for constructing a Metadata Object DTO. */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Builder for constructing a Metadata Object DTO. */
+ public static class Builder {
+
+ private final MetadataObjectDTO metadataObjectDTO = new
MetadataObjectDTO();
+
+ /**
+ * Sets the parent of the metadata object.
+ *
+ * @param parent The parent of the metadata object.
+ * @return The builder.
+ */
+ public Builder withParent(String parent) {
+ metadataObjectDTO.parent = parent;
+ return this;
+ }
+
+ /**
+ * Sets the name of the metadata object.
+ *
+ * @param name The name of the metadata object.
+ * @return The builder.
+ */
+ public Builder withName(String name) {
+ metadataObjectDTO.name = name;
+ return this;
+ }
+
+ /**
+ * Sets the type of the metadata object.
+ *
+ * @param type The type of the metadata object.
+ * @return The builder.
+ */
+ public Builder withType(Type type) {
+ metadataObjectDTO.type = type;
+ return this;
+ }
+
+ /** @return The constructed Metadata Object DTO. */
+ public MetadataObjectDTO build() {
+ return metadataObjectDTO;
+ }
+ }
+}
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
new file mode 100644
index 000000000..a7f25480b
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/tag/TagDTO.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.dto.tag;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+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")
+ private String name;
+
+ @JsonProperty("comment")
+ private String comment;
+
+ @JsonProperty("properties")
+ private Map<String, String> properties;
+
+ @JsonProperty("audit")
+ private AuditDTO audit;
+
+ @JsonProperty("inherited")
+ private Optional<Boolean> inherited = Optional.empty();
+
+ private TagDTO() {}
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public String comment() {
+ return comment;
+ }
+
+ @Override
+ public Map<String, String> properties() {
+ return properties;
+ }
+
+ @Override
+ public AuditDTO auditInfo() {
+ return audit;
+ }
+
+ @Override
+ public Optional<Boolean> inherited() {
+ return inherited;
+ }
+
+ /** @return a new builder for constructing a Tag DTO. */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Builder class for constructing TagDTO instances. */
+ public static class Builder {
+ private final TagDTO tagDTO;
+
+ private Builder() {
+ tagDTO = new TagDTO();
+ }
+
+ /**
+ * Sets the name of the tag.
+ *
+ * @param name The name of the tag.
+ * @return The builder instance.
+ */
+ public Builder withName(String name) {
+ tagDTO.name = name;
+ return this;
+ }
+
+ /**
+ * Sets the comment associated with the tag.
+ *
+ * @param comment The comment associated with the tag.
+ * @return The builder instance.
+ */
+ public Builder withComment(String comment) {
+ tagDTO.comment = comment;
+ return this;
+ }
+
+ /**
+ * Sets the properties associated with the tag.
+ *
+ * @param properties The properties associated with the tag.
+ * @return The builder instance.
+ */
+ public Builder withProperties(Map<String, String> properties) {
+ tagDTO.properties = properties;
+ return this;
+ }
+
+ /**
+ * Sets the audit information for the tag.
+ *
+ * @param audit The audit information for the tag.
+ * @return The builder instance.
+ */
+ public Builder withAudit(AuditDTO audit) {
+ tagDTO.audit = audit;
+ return this;
+ }
+
+ /**
+ * Sets whether the tag is inherited.
+ *
+ * @param inherited Whether the tag is inherited.
+ * @return The builder instance.
+ */
+ public Builder withInherited(Optional<Boolean> inherited) {
+ tagDTO.inherited = inherited;
+ return this;
+ }
+
+ /** @return The constructed Tag DTO. */
+ public TagDTO build() {
+ return tagDTO;
+ }
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
index 5e253788a..fa6ae6b56 100644
--- a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
+++ b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
@@ -22,9 +22,11 @@ import static
org.apache.gravitino.rel.expressions.transforms.Transforms.NAME_OF
import java.util.Arrays;
import java.util.Map;
+import java.util.Optional;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.gravitino.Audit;
import org.apache.gravitino.Catalog;
+import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.Metalake;
import org.apache.gravitino.Schema;
import org.apache.gravitino.authorization.Group;
@@ -68,6 +70,8 @@ import
org.apache.gravitino.dto.rel.partitions.IdentityPartitionDTO;
import org.apache.gravitino.dto.rel.partitions.ListPartitionDTO;
import org.apache.gravitino.dto.rel.partitions.PartitionDTO;
import org.apache.gravitino.dto.rel.partitions.RangePartitionDTO;
+import org.apache.gravitino.dto.tag.MetadataObjectDTO;
+import org.apache.gravitino.dto.tag.TagDTO;
import org.apache.gravitino.file.Fileset;
import org.apache.gravitino.messaging.Topic;
import org.apache.gravitino.rel.Column;
@@ -92,6 +96,7 @@ import org.apache.gravitino.rel.partitions.Partition;
import org.apache.gravitino.rel.partitions.Partitions;
import org.apache.gravitino.rel.partitions.RangePartition;
import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.tag.Tag;
/** Utility class for converting between DTOs and domain objects. */
public class DTOConverters {
@@ -465,6 +470,39 @@ public class DTOConverters {
.build();
}
+ /**
+ * Converts a MetadataObject to a MetadataObjectDTO.
+ *
+ * @param metadataObject The metadata object to be converted.
+ * @return The metadata object DTO.
+ */
+ public static MetadataObjectDTO toDTO(MetadataObject metadataObject) {
+ return MetadataObjectDTO.builder()
+ .withParent(metadataObject.parent())
+ .withName(metadataObject.name())
+ .withType(metadataObject.type())
+ .build();
+ }
+
+ /**
+ * Converts a Tag to a TagDTO.
+ *
+ * @param tag The tag to be converted.
+ * @param inherited The inherited flag.
+ * @return The tag DTO.
+ */
+ public static TagDTO toDTO(Tag tag, Optional<Boolean> inherited) {
+ TagDTO.Builder builder =
+ TagDTO.builder()
+ .withName(tag.name())
+ .withComment(tag.comment())
+ .withProperties(tag.properties())
+ .withAudit(toDTO(tag.auditInfo()))
+ .withInherited(inherited);
+
+ return builder.build();
+ }
+
/**
* Converts a Expression to an FunctionArg DTO.
*
diff --git a/common/src/main/java/org/apache/gravitino/json/JsonUtils.java
b/common/src/main/java/org/apache/gravitino/json/JsonUtils.java
index 76ba31163..b3f81cb67 100644
--- a/common/src/main/java/org/apache/gravitino/json/JsonUtils.java
+++ b/common/src/main/java/org/apache/gravitino/json/JsonUtils.java
@@ -34,6 +34,7 @@ import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.cfg.EnumFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
@@ -252,7 +253,8 @@ public class JsonUtils {
.configure(EnumFeature.WRITE_ENUMS_TO_LOWERCASE, true)
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.build()
- .registerModule(new JavaTimeModule());
+ .registerModule(new JavaTimeModule())
+ .registerModule(new Jdk8Module());
}
/**
@@ -288,7 +290,8 @@ public class JsonUtils {
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.build()
.setVisibility(PropertyAccessor.FIELD,
JsonAutoDetect.Visibility.ANY)
- .registerModule(new JavaTimeModule());
+ .registerModule(new JavaTimeModule())
+ .registerModule(new Jdk8Module());
}
/**
diff --git
a/common/src/test/java/org/apache/gravitino/dto/requests/TestTagCreateRequest.java
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagCreateRequest.java
new file mode 100644
index 000000000..5d90639ca
--- /dev/null
+++
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagCreateRequest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.dto.requests;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagCreateRequest {
+
+ @Test
+ public void testTagCreateRequestSerDe() throws JsonProcessingException {
+ TagCreateRequest request = new TagCreateRequest("tag_test", "tag comment",
null);
+ String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+ TagCreateRequest deserRequest =
+ JsonUtils.objectMapper().readValue(serJson, TagCreateRequest.class);
+ Assertions.assertEquals(request, deserRequest);
+ Assertions.assertEquals("tag_test", deserRequest.getName());
+ Assertions.assertEquals("tag comment", deserRequest.getComment());
+ Assertions.assertNull(deserRequest.getProperties());
+
+ Map<String, String> properties = ImmutableMap.of("key", "value");
+ TagCreateRequest request1 = new TagCreateRequest("tag_test", "tag
comment", properties);
+ serJson = JsonUtils.objectMapper().writeValueAsString(request1);
+ TagCreateRequest deserRequest1 =
+ JsonUtils.objectMapper().readValue(serJson, TagCreateRequest.class);
+ Assertions.assertEquals(request1, deserRequest1);
+ Assertions.assertEquals(properties, deserRequest1.getProperties());
+ }
+}
diff --git
a/common/src/test/java/org/apache/gravitino/dto/requests/TestTagUpdatesRequest.java
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagUpdatesRequest.java
new file mode 100644
index 000000000..8fb0a0bae
--- /dev/null
+++
b/common/src/test/java/org/apache/gravitino/dto/requests/TestTagUpdatesRequest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.dto.requests;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagUpdatesRequest {
+
+ @Test
+ public void testRenameTagRequestSerDe() throws JsonProcessingException {
+ TagUpdateRequest.RenameTagRequest request =
+ new TagUpdateRequest.RenameTagRequest("tag_test_new");
+ String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+ TagUpdateRequest.RenameTagRequest deserRequest =
+ JsonUtils.objectMapper().readValue(serJson,
TagUpdateRequest.RenameTagRequest.class);
+ Assertions.assertEquals(request, deserRequest);
+ Assertions.assertEquals("tag_test_new", deserRequest.getNewName());
+ }
+
+ @Test
+ public void testUpdateTagCommentRequestSerDe() throws
JsonProcessingException {
+ TagUpdateRequest.UpdateTagCommentRequest request =
+ new TagUpdateRequest.UpdateTagCommentRequest("tag comment new");
+ String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+ TagUpdateRequest.UpdateTagCommentRequest deserRequest =
+ JsonUtils.objectMapper().readValue(serJson,
TagUpdateRequest.UpdateTagCommentRequest.class);
+ Assertions.assertEquals(request, deserRequest);
+ Assertions.assertEquals("tag comment new", deserRequest.getNewComment());
+ }
+
+ @Test
+ public void testSetTagPropertyRequestSerDe() throws JsonProcessingException {
+ TagUpdateRequest.SetTagPropertyRequest request =
+ new TagUpdateRequest.SetTagPropertyRequest("key", "value");
+ String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+ TagUpdateRequest.SetTagPropertyRequest deserRequest =
+ JsonUtils.objectMapper().readValue(serJson,
TagUpdateRequest.SetTagPropertyRequest.class);
+ Assertions.assertEquals(request, deserRequest);
+ Assertions.assertEquals("key", deserRequest.getProperty());
+ Assertions.assertEquals("value", deserRequest.getValue());
+ }
+
+ @Test
+ public void testRemoveTagPropertyRequestSerDe() throws
JsonProcessingException {
+ TagUpdateRequest.RemoveTagPropertyRequest request =
+ new TagUpdateRequest.RemoveTagPropertyRequest("key");
+ String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+ TagUpdateRequest.RemoveTagPropertyRequest deserRequest =
+ JsonUtils.objectMapper()
+ .readValue(serJson,
TagUpdateRequest.RemoveTagPropertyRequest.class);
+ Assertions.assertEquals(request, deserRequest);
+ Assertions.assertEquals("key", deserRequest.getProperty());
+ }
+
+ @Test
+ public void testTagUpdatesRequestSerDe() throws JsonProcessingException {
+ TagUpdateRequest request = new
TagUpdateRequest.RenameTagRequest("tag_test_new");
+ TagUpdateRequest request1 = new
TagUpdateRequest.UpdateTagCommentRequest("tag comment new");
+ TagUpdateRequest request2 = new
TagUpdateRequest.SetTagPropertyRequest("key", "value");
+ TagUpdateRequest request3 = new
TagUpdateRequest.RemoveTagPropertyRequest("key");
+
+ List<TagUpdateRequest> updates = ImmutableList.of(request, request1,
request2, request3);
+ TagUpdatesRequest tagUpdatesRequest = new TagUpdatesRequest(updates);
+ String serJson =
JsonUtils.objectMapper().writeValueAsString(tagUpdatesRequest);
+ TagUpdatesRequest deserRequest =
+ JsonUtils.objectMapper().readValue(serJson, TagUpdatesRequest.class);
+ Assertions.assertEquals(tagUpdatesRequest, deserRequest);
+ Assertions.assertEquals(updates, deserRequest.getUpdates());
+ }
+}
diff --git
a/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
b/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
index 7f9ebfeb7..242c0f953 100644
--- a/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
+++ b/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
@@ -18,11 +18,14 @@
*/
package org.apache.gravitino.dto.responses;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.Lists;
import java.time.Instant;
import org.apache.gravitino.Catalog;
@@ -41,7 +44,9 @@ import org.apache.gravitino.dto.authorization.UserDTO;
import org.apache.gravitino.dto.rel.ColumnDTO;
import org.apache.gravitino.dto.rel.TableDTO;
import org.apache.gravitino.dto.rel.partitioning.Partitioning;
+import org.apache.gravitino.dto.tag.TagDTO;
import org.apache.gravitino.dto.util.DTOConverters;
+import org.apache.gravitino.json.JsonUtils;
import org.apache.gravitino.rel.types.Types;
import org.junit.jupiter.api.Test;
@@ -320,4 +325,56 @@ public class TestResponses {
RoleResponse role = new RoleResponse();
assertThrows(IllegalArgumentException.class, () -> role.validate());
}
+
+ @Test
+ void testNameListResponse() throws JsonProcessingException {
+ String[] names = new String[] {"name1", "name2"};
+ NameListResponse response = new NameListResponse(names);
+ assertDoesNotThrow(response::validate);
+
+ String serJson = JsonUtils.objectMapper().writeValueAsString(response);
+ NameListResponse deserResponse =
+ JsonUtils.objectMapper().readValue(serJson, NameListResponse.class);
+ assertEquals(response, deserResponse);
+ assertArrayEquals(names, deserResponse.getNames());
+
+ NameListResponse response1 = new NameListResponse();
+ Exception e = assertThrows(IllegalArgumentException.class,
response1::validate);
+ assertEquals("\"names\" must not be null", e.getMessage());
+ }
+
+ @Test
+ void testTagListResponse() throws JsonProcessingException {
+ TagDTO tag1 =
TagDTO.builder().withName("tag1").withComment("comment1").build();
+ TagDTO tag2 =
TagDTO.builder().withName("tag2").withComment("comment2").build();
+ TagDTO[] tags = new TagDTO[] {tag1, tag2};
+ TagListResponse response = new TagListResponse(tags);
+ assertDoesNotThrow(response::validate);
+
+ String serJson = JsonUtils.objectMapper().writeValueAsString(response);
+ TagListResponse deserResponse =
+ JsonUtils.objectMapper().readValue(serJson, TagListResponse.class);
+ assertEquals(response, deserResponse);
+ assertArrayEquals(tags, deserResponse.getTags());
+
+ TagListResponse response1 = new TagListResponse();
+ Exception e = assertThrows(IllegalArgumentException.class,
response1::validate);
+ assertEquals("\"tags\" must not be null", e.getMessage());
+ }
+
+ @Test
+ void testTagResponse() throws JsonProcessingException {
+ TagDTO tag =
TagDTO.builder().withName("tag1").withComment("comment1").build();
+ TagResponse response = new TagResponse(tag);
+ assertDoesNotThrow(response::validate);
+
+ String serJson = JsonUtils.objectMapper().writeValueAsString(response);
+ TagResponse deserResponse = JsonUtils.objectMapper().readValue(serJson,
TagResponse.class);
+ assertEquals(response, deserResponse);
+ assertEquals(tag, deserResponse.getTag());
+
+ TagResponse response1 = new TagResponse();
+ Exception e = assertThrows(IllegalArgumentException.class,
response1::validate);
+ assertEquals("\"tag\" must not be null", e.getMessage());
+ }
}
diff --git
a/common/src/test/java/org/apache/gravitino/dto/tag/TestMetadataObjectDTO.java
b/common/src/test/java/org/apache/gravitino/dto/tag/TestMetadataObjectDTO.java
new file mode 100644
index 000000000..573890cf1
--- /dev/null
+++
b/common/src/test/java/org/apache/gravitino/dto/tag/TestMetadataObjectDTO.java
@@ -0,0 +1,150 @@
+/*
+ * 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.dto.tag;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestMetadataObjectDTO {
+
+ @Test
+ void testObjectSerDe() throws JsonProcessingException {
+
+ // Test metalake object
+ MetadataObjectDTO metalakeDTO =
+ MetadataObjectDTO.builder()
+ .withName("metalake_test")
+ .withType(MetadataObject.Type.METALAKE)
+ .build();
+
+ String serJson = JsonUtils.objectMapper().writeValueAsString(metalakeDTO);
+ String expected = "{\"fullName\":\"metalake_test\",\"type\":\"metalake\"}";
+ Assertions.assertEquals(
+ JsonUtils.objectMapper().readTree(expected),
JsonUtils.objectMapper().readTree(serJson));
+
+ MetadataObjectDTO deserMetalakeDTO =
+ JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+ Assertions.assertEquals(metalakeDTO, deserMetalakeDTO);
+
+ // Test catalog object
+ MetadataObjectDTO catalogDTO =
+ MetadataObjectDTO.builder()
+ .withName("catalog_test")
+ .withType(MetadataObject.Type.CATALOG)
+ .build();
+
+ serJson = JsonUtils.objectMapper().writeValueAsString(catalogDTO);
+ expected = "{\"fullName\":\"catalog_test\",\"type\":\"catalog\"}";
+ Assertions.assertEquals(
+ JsonUtils.objectMapper().readTree(expected),
JsonUtils.objectMapper().readTree(serJson));
+
+ MetadataObjectDTO deserCatalogDTO =
+ JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+ Assertions.assertEquals(catalogDTO, deserCatalogDTO);
+
+ // Test schema object
+ MetadataObjectDTO schemaDTO =
+ MetadataObjectDTO.builder()
+ .withName("schema_test")
+ .withParent("catalog_test")
+ .withType(MetadataObject.Type.SCHEMA)
+ .build();
+
+ serJson = JsonUtils.objectMapper().writeValueAsString(schemaDTO);
+ expected =
"{\"fullName\":\"catalog_test.schema_test\",\"type\":\"schema\"}";
+ Assertions.assertEquals(
+ JsonUtils.objectMapper().readTree(expected),
JsonUtils.objectMapper().readTree(serJson));
+
+ MetadataObjectDTO deserSchemaDTO =
+ JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+ Assertions.assertEquals(schemaDTO, deserSchemaDTO);
+
+ // Test table object
+ MetadataObjectDTO tableDTO =
+ MetadataObjectDTO.builder()
+ .withName("table_test")
+ .withParent("catalog_test.schema_test")
+ .withType(MetadataObject.Type.TABLE)
+ .build();
+
+ serJson = JsonUtils.objectMapper().writeValueAsString(tableDTO);
+ expected =
"{\"fullName\":\"catalog_test.schema_test.table_test\",\"type\":\"table\"}";
+ Assertions.assertEquals(
+ JsonUtils.objectMapper().readTree(expected),
JsonUtils.objectMapper().readTree(serJson));
+
+ MetadataObjectDTO deserTableDTO =
+ JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+ Assertions.assertEquals(tableDTO, deserTableDTO);
+
+ // Test column object
+ MetadataObjectDTO columnDTO =
+ MetadataObjectDTO.builder()
+ .withName("column_test")
+ .withParent("catalog_test.schema_test.table_test")
+ .withType(MetadataObject.Type.COLUMN)
+ .build();
+
+ serJson = JsonUtils.objectMapper().writeValueAsString(columnDTO);
+ expected =
+
"{\"fullName\":\"catalog_test.schema_test.table_test.column_test\",\"type\":\"column\"}";
+ Assertions.assertEquals(
+ JsonUtils.objectMapper().readTree(expected),
JsonUtils.objectMapper().readTree(serJson));
+
+ MetadataObjectDTO deserColumnDTO =
+ JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+ Assertions.assertEquals(columnDTO, deserColumnDTO);
+
+ // Test topic object
+ MetadataObjectDTO topicDTO =
+ MetadataObjectDTO.builder()
+ .withName("topic_test")
+ .withParent("catalog_test.schema_test")
+ .withType(MetadataObject.Type.TOPIC)
+ .build();
+
+ serJson = JsonUtils.objectMapper().writeValueAsString(topicDTO);
+ expected =
"{\"fullName\":\"catalog_test.schema_test.topic_test\",\"type\":\"topic\"}";
+ Assertions.assertEquals(
+ JsonUtils.objectMapper().readTree(expected),
JsonUtils.objectMapper().readTree(serJson));
+
+ MetadataObjectDTO deserTopicDTO =
+ JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+ Assertions.assertEquals(topicDTO, deserTopicDTO);
+
+ // Test fileset object
+ MetadataObjectDTO filesetDTO =
+ MetadataObjectDTO.builder()
+ .withName("fileset_test")
+ .withParent("catalog_test.schema_test")
+ .withType(MetadataObject.Type.FILESET)
+ .build();
+
+ serJson = JsonUtils.objectMapper().writeValueAsString(filesetDTO);
+ expected =
"{\"fullName\":\"catalog_test.schema_test.fileset_test\",\"type\":\"fileset\"}";
+ Assertions.assertEquals(
+ JsonUtils.objectMapper().readTree(expected),
JsonUtils.objectMapper().readTree(serJson));
+
+ MetadataObjectDTO deserFilesetDTO =
+ JsonUtils.objectMapper().readValue(serJson, MetadataObjectDTO.class);
+ Assertions.assertEquals(filesetDTO, deserFilesetDTO);
+ }
+}
diff --git a/common/src/test/java/org/apache/gravitino/dto/tag/TestTagDTO.java
b/common/src/test/java/org/apache/gravitino/dto/tag/TestTagDTO.java
new file mode 100644
index 000000000..89acaa887
--- /dev/null
+++ b/common/src/test/java/org/apache/gravitino/dto/tag/TestTagDTO.java
@@ -0,0 +1,103 @@
+/*
+ * 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.dto.tag;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
+import java.time.Instant;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagDTO {
+
+ @Test
+ public void testTagSerDe() throws JsonProcessingException {
+ AuditDTO audit =
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+
+ TagDTO tagDTO =
+ TagDTO.builder().withName("tag_test").withComment("tag
comment").withAudit(audit).build();
+
+ String serJson = JsonUtils.objectMapper().writeValueAsString(tagDTO);
+ TagDTO deserTagDTO = JsonUtils.objectMapper().readValue(serJson,
TagDTO.class);
+ Assertions.assertEquals(tagDTO, deserTagDTO);
+
+ Assertions.assertEquals("tag_test", deserTagDTO.name());
+ Assertions.assertEquals("tag comment", deserTagDTO.comment());
+ Assertions.assertEquals(audit, deserTagDTO.auditInfo());
+ Assertions.assertNull(deserTagDTO.properties());
+
+ // Test tag with property
+ Map<String, String> properties = ImmutableMap.of("key", "value");
+ TagDTO tagDTO1 =
+ TagDTO.builder()
+ .withName("tag_test")
+ .withComment("tag comment")
+ .withAudit(audit)
+ .withProperties(properties)
+ .build();
+
+ serJson = JsonUtils.objectMapper().writeValueAsString(tagDTO1);
+ TagDTO deserTagDTO1 = JsonUtils.objectMapper().readValue(serJson,
TagDTO.class);
+ Assertions.assertEquals(tagDTO1, deserTagDTO1);
+
+ Assertions.assertEquals(properties, deserTagDTO1.properties());
+
+ // Test tag with inherited
+ TagDTO tagDTO2 =
+ TagDTO.builder()
+ .withName("tag_test")
+ .withComment("tag comment")
+ .withAudit(audit)
+ .withInherited(Optional.empty())
+ .build();
+
+ serJson = JsonUtils.objectMapper().writeValueAsString(tagDTO2);
+ TagDTO deserTagDTO2 = JsonUtils.objectMapper().readValue(serJson,
TagDTO.class);
+ Assertions.assertEquals(tagDTO2, deserTagDTO2);
+ Assertions.assertEquals(Optional.empty(), deserTagDTO2.inherited());
+
+ TagDTO tagDTO3 =
+ TagDTO.builder()
+ .withName("tag_test")
+ .withComment("tag comment")
+ .withAudit(audit)
+ .withInherited(Optional.of(false))
+ .build();
+
+ serJson = JsonUtils.objectMapper().writeValueAsString(tagDTO3);
+ TagDTO deserTagDTO3 = JsonUtils.objectMapper().readValue(serJson,
TagDTO.class);
+ Assertions.assertEquals(Optional.of(false), deserTagDTO3.inherited());
+
+ TagDTO tagDTO4 =
+ TagDTO.builder()
+ .withName("tag_test")
+ .withComment("tag comment")
+ .withAudit(audit)
+ .withInherited(Optional.of(true))
+ .build();
+
+ serJson = JsonUtils.objectMapper().writeValueAsString(tagDTO4);
+ TagDTO deserTagDTO4 = JsonUtils.objectMapper().readValue(serJson,
TagDTO.class);
+ Assertions.assertEquals(Optional.of(true), deserTagDTO4.inherited());
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/tag/SupportsTagOperations.java
b/core/src/main/java/org/apache/gravitino/tag/SupportsTagOperations.java
index fd635a024..eaea94e35 100644
--- a/core/src/main/java/org/apache/gravitino/tag/SupportsTagOperations.java
+++ b/core/src/main/java/org/apache/gravitino/tag/SupportsTagOperations.java
@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package org.apache.gravitino.tag;
import java.io.IOException;
diff --git a/core/src/main/java/org/apache/gravitino/tag/TagManager.java
b/core/src/main/java/org/apache/gravitino/tag/TagManager.java
index 1dc65e2d8..040ac9f19 100644
--- a/core/src/main/java/org/apache/gravitino/tag/TagManager.java
+++ b/core/src/main/java/org/apache/gravitino/tag/TagManager.java
@@ -86,6 +86,10 @@ public class TagManager {
}
public String[] listTags(String metalake) {
+ return
Arrays.stream(listTagsInfo(metalake)).map(Tag::name).toArray(String[]::new);
+ }
+
+ public Tag[] listTagsInfo(String metalake) {
return TreeLockUtils.doWithTreeLock(
NameIdentifier.of(ofTagNamespace(metalake).levels()),
LockType.READ,
@@ -95,8 +99,7 @@ public class TagManager {
try {
return entityStore
.list(ofTagNamespace(metalake), TagEntity.class,
Entity.EntityType.TAG).stream()
- .map(TagEntity::name)
- .toArray(String[]::new);
+ .toArray(Tag[]::new);
} catch (IOException ioe) {
LOG.error("Failed to list tags under metalake {}", metalake, ioe);
throw new RuntimeException(ioe);
diff --git
a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
index 4d025e9ec..cf1bb7aac 100644
--- a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
+++ b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
@@ -44,6 +44,7 @@ import
org.apache.gravitino.server.web.mapper.JsonMappingExceptionMapper;
import org.apache.gravitino.server.web.mapper.JsonParseExceptionMapper;
import org.apache.gravitino.server.web.mapper.JsonProcessingExceptionMapper;
import org.apache.gravitino.server.web.ui.WebUIFilter;
+import org.apache.gravitino.tag.TagManager;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
@@ -97,12 +98,12 @@ public class GravitinoServer extends ResourceConfig {
protected void configure() {
bind(gravitinoEnv.metalakeDispatcher()).to(MetalakeDispatcher.class).ranked(1);
bind(gravitinoEnv.catalogDispatcher()).to(CatalogDispatcher.class).ranked(1);
-
bind(gravitinoEnv.schemaDispatcher()).to(SchemaDispatcher.class).ranked(1);
bind(gravitinoEnv.tableDispatcher()).to(TableDispatcher.class).ranked(1);
bind(gravitinoEnv.partitionDispatcher()).to(PartitionDispatcher.class).ranked(1);
bind(gravitinoEnv.filesetDispatcher()).to(FilesetDispatcher.class).ranked(1);
bind(gravitinoEnv.topicDispatcher()).to(TopicDispatcher.class).ranked(1);
+ bind(gravitinoEnv.tagManager()).to(TagManager.class).ranked(1);
}
});
register(JsonProcessingExceptionMapper.class);
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/ObjectMapperProvider.java
b/server/src/main/java/org/apache/gravitino/server/web/ObjectMapperProvider.java
index 354c62319..fdf8ac911 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/ObjectMapperProvider.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/ObjectMapperProvider.java
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.EnumFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
@@ -39,7 +40,8 @@ public class ObjectMapperProvider implements
ContextResolver<ObjectMapper> {
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.build()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
- .registerModule(new JavaTimeModule());
+ .registerModule(new JavaTimeModule())
+ .registerModule(new Jdk8Module());
}
/**
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
index 677c42de9..459938ee9 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
@@ -35,6 +35,8 @@ import
org.apache.gravitino.exceptions.PartitionAlreadyExistsException;
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.UserAlreadyExistsException;
import org.apache.gravitino.server.web.Utils;
@@ -107,6 +109,11 @@ public class ExceptionHandlers {
return GroupPermissionOperationExceptionHandler.INSTANCE.handle(op, roles,
parent, e);
}
+ public static Response handleTagException(
+ OperationType op, String tag, String parent, Exception e) {
+ return TagExceptionHandler.INSTANCE.handle(op, tag, parent, e);
+ }
+
public static Response handleTestConnectionException(Exception e) {
ErrorResponse response;
if (e instanceof IllegalArgumentException) {
@@ -519,6 +526,41 @@ public class ExceptionHandlers {
}
}
+ private static class TagExceptionHandler extends BaseExceptionHandler {
+
+ private static final ExceptionHandler INSTANCE = new TagExceptionHandler();
+
+ private static String getTagErrorMsg(
+ String tag, String operation, String parent, String reason) {
+ return String.format(
+ "Failed to operate tag(s)%s operation [%s] under object [%s], reason
[%s]",
+ tag, operation, parent, reason);
+ }
+
+ @Override
+ public Response handle(OperationType op, String tag, String parent,
Exception e) {
+ String formatted = StringUtil.isBlank(tag) ? "" : " [" + tag + "]";
+ String errorMsg = getTagErrorMsg(formatted, op.name(), parent,
getErrorMsg(e));
+ LOG.warn(errorMsg, e);
+
+ if (e instanceof IllegalArgumentException) {
+ return Utils.illegalArguments(errorMsg, e);
+
+ } else if (e instanceof NotFoundException) {
+ return Utils.notFound(errorMsg, e);
+
+ } else if (e instanceof TagAlreadyExistsException) {
+ return Utils.alreadyExists(errorMsg, e);
+
+ } else if (e instanceof TagAlreadyAssociatedException) {
+ return Utils.alreadyExists(errorMsg, e);
+
+ } else {
+ return super.handle(op, tag, parent, e);
+ }
+ }
+ }
+
@VisibleForTesting
static class BaseExceptionHandler extends ExceptionHandler {
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java
index 0adc4f831..9e611f6e2 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java
@@ -30,5 +30,6 @@ public enum OperationType {
REMOVE,
DELETE,
GRANT,
- REVOKE
+ REVOKE,
+ ASSOCIATE,
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
new file mode 100644
index 000000000..4a3abe7f9
--- /dev/null
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/TagOperations.java
@@ -0,0 +1,451 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.server.web.rest;
+
+import com.codahale.metrics.annotation.ResponseMetered;
+import com.codahale.metrics.annotation.Timed;
+import com.google.common.collect.Lists;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.dto.requests.TagCreateRequest;
+import org.apache.gravitino.dto.requests.TagUpdateRequest;
+import org.apache.gravitino.dto.requests.TagUpdatesRequest;
+import org.apache.gravitino.dto.requests.TagsAssociateRequest;
+import org.apache.gravitino.dto.responses.DropResponse;
+import org.apache.gravitino.dto.responses.MetadataObjectListResponse;
+import org.apache.gravitino.dto.responses.NameListResponse;
+import org.apache.gravitino.dto.responses.TagListResponse;
+import org.apache.gravitino.dto.responses.TagResponse;
+import org.apache.gravitino.dto.tag.MetadataObjectDTO;
+import org.apache.gravitino.dto.tag.TagDTO;
+import org.apache.gravitino.dto.util.DTOConverters;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagChange;
+import org.apache.gravitino.tag.TagManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Path("metalakes/{metalake}/tags")
+public class TagOperations {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(TagOperations.class);
+
+ private final TagManager tagManager;
+
+ @Context private HttpServletRequest httpRequest;
+
+ @Inject
+ public TagOperations(TagManager tagManager) {
+ this.tagManager = tagManager;
+ }
+
+ @GET
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "list-tags." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
+ @ResponseMetered(name = "list-tags", absolute = true)
+ public Response listTags(
+ @PathParam("metalake") String metalake,
+ @QueryParam("details") @DefaultValue("false") boolean verbose) {
+ LOG.info(
+ "Received list tag {} request for metalake: {}", verbose ? "infos" :
"names", metalake);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ if (verbose) {
+ Tag[] tags = tagManager.listTagsInfo(metalake);
+ TagDTO[] tagDTOs;
+ if (ArrayUtils.isEmpty(tags)) {
+ tagDTOs = new TagDTO[0];
+ } else {
+ tagDTOs =
+ Arrays.stream(tags)
+ .map(t -> DTOConverters.toDTO(t, Optional.empty()))
+ .toArray(TagDTO[]::new);
+ }
+
+ LOG.info("List {} tags info under metalake: {}", tagDTOs.length,
metalake);
+ return Utils.ok(new TagListResponse(tagDTOs));
+
+ } else {
+ String[] tagNames = tagManager.listTags(metalake);
+ tagNames = tagNames == null ? new String[0] : tagNames;
+
+ LOG.info("List {} tags under metalake: {}", tagNames.length,
metalake);
+ return Utils.ok(new NameListResponse(tagNames));
+ }
+ });
+ } catch (Exception e) {
+ return ExceptionHandlers.handleTagException(OperationType.LIST, "",
metalake, e);
+ }
+ }
+
+ @POST
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "create-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
+ @ResponseMetered(name = "create-tag", absolute = true)
+ public Response createTag(@PathParam("metalake") String metalake,
TagCreateRequest request) {
+ LOG.info("Received create tag request under metalake: {}", metalake);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ request.validate();
+ Tag tag =
+ tagManager.createTag(
+ metalake, request.getName(), request.getComment(),
request.getProperties());
+
+ LOG.info("Created tag: {} under metalake: {}", tag.name(),
metalake);
+ return Utils.ok(new TagResponse(DTOConverters.toDTO(tag,
Optional.empty())));
+ });
+ } catch (Exception e) {
+ return ExceptionHandlers.handleTagException(
+ OperationType.CREATE, request.getName(), metalake, e);
+ }
+ }
+
+ @GET
+ @Path("{tag}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "get-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
+ @ResponseMetered(name = "get-tag", absolute = true)
+ public Response getTag(@PathParam("metalake") String metalake,
@PathParam("tag") String name) {
+ LOG.info("Received get tag request for tag: {} under metalake: {}", name,
metalake);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ Tag tag = tagManager.getTag(metalake, name);
+ LOG.info("Get tag: {} under metalake: {}", name, metalake);
+ return Utils.ok(new TagResponse(DTOConverters.toDTO(tag,
Optional.empty())));
+ });
+ } catch (Exception e) {
+ return ExceptionHandlers.handleTagException(OperationType.GET, name,
metalake, e);
+ }
+ }
+
+ @PUT
+ @Path("{tag}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "alter-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
+ @ResponseMetered(name = "alter-tag", absolute = true)
+ public Response alterTag(
+ @PathParam("metalake") String metalake,
+ @PathParam("tag") String name,
+ TagUpdatesRequest request) {
+ LOG.info("Received alter tag request for tag: {} under metalake: {}",
name, metalake);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ request.validate();
+
+ TagChange[] changes =
+ request.getUpdates().stream()
+ .map(TagUpdateRequest::tagChange)
+ .toArray(TagChange[]::new);
+ Tag tag = tagManager.alterTag(metalake, name, changes);
+
+ LOG.info("Altered tag: {} under metalake: {}", name, metalake);
+ return Utils.ok(new TagResponse(DTOConverters.toDTO(tag,
Optional.empty())));
+ });
+ } catch (Exception e) {
+ return ExceptionHandlers.handleTagException(OperationType.ALTER, name,
metalake, e);
+ }
+ }
+
+ @DELETE
+ @Path("{tag}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "delete-tag." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
+ @ResponseMetered(name = "delete-tag", absolute = true)
+ public Response deleteTag(@PathParam("metalake") String metalake,
@PathParam("tag") String name) {
+ LOG.info("Received delete tag request for tag: {} under metalake: {}",
name, metalake);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ boolean deleted = tagManager.deleteTag(metalake, name);
+ if (!deleted) {
+ LOG.warn("Failed to delete tag {} under metalake {}", name,
metalake);
+ } else {
+ LOG.info("Deleted tag: {} under metalake: {}", name, metalake);
+ }
+
+ return Utils.ok(new DropResponse(deleted));
+ });
+ } catch (Exception e) {
+ return ExceptionHandlers.handleTagException(OperationType.DELETE, name,
metalake, e);
+ }
+ }
+
+ @GET
+ @Path("{type}/{fullName}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "list-object-tags." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "list-object-tags", absolute = true)
+ public Response listTagsForMetadataObject(
+ @PathParam("metalake") String metalake,
+ @PathParam("type") String type,
+ @PathParam("fullName") String fullName,
+ @QueryParam("details") @DefaultValue("false") boolean verbose) {
+ LOG.info(
+ "Received list tag {} request for object type: {}, full name: {} under
metalake: {}",
+ verbose ? "infos" : "names",
+ type,
+ fullName,
+ metalake);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ MetadataObject object =
+ MetadataObjects.parse(
+ fullName,
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+
+ List<TagDTO> tags = Lists.newArrayList();
+ Tag[] nonInheritedTags =
tagManager.listTagsInfoForMetadataObject(metalake, object);
+ if (ArrayUtils.isNotEmpty(nonInheritedTags)) {
+ Collections.addAll(
+ tags,
+ Arrays.stream(nonInheritedTags)
+ .map(t -> DTOConverters.toDTO(t, Optional.of(false)))
+ .toArray(TagDTO[]::new));
+ }
+
+ MetadataObject parentObject = MetadataObjects.parent(object);
+ while (parentObject != null) {
+ Tag[] inheritedTags =
+ tagManager.listTagsInfoForMetadataObject(metalake,
parentObject);
+ if (ArrayUtils.isNotEmpty(inheritedTags)) {
+ Collections.addAll(
+ tags,
+ Arrays.stream(inheritedTags)
+ .map(t -> DTOConverters.toDTO(t, Optional.of(true)))
+ .toArray(TagDTO[]::new));
+ }
+ parentObject = MetadataObjects.parent(parentObject);
+ }
+
+ if (verbose) {
+ LOG.info(
+ "List {} tags info for object type: {}, full name: {} under
metalake: {}",
+ tags.size(),
+ type,
+ fullName,
+ metalake);
+ return Utils.ok(new TagListResponse(tags.toArray(new
TagDTO[0])));
+
+ } else {
+ String[] tagNames =
tags.stream().map(TagDTO::name).toArray(String[]::new);
+
+ LOG.info(
+ "List {} tags for object type: {}, full name: {} under
metalake: {}",
+ tagNames.length,
+ type,
+ fullName,
+ metalake);
+ return Utils.ok(new NameListResponse(tagNames));
+ }
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleTagException(OperationType.LIST, "",
fullName, e);
+ }
+ }
+
+ @GET
+ @Path("{type}/{fullName}/{tag}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "get-object-tag." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "get-object-tag", absolute = true)
+ public Response getTagForObject(
+ @PathParam("metalake") String metalake,
+ @PathParam("type") String type,
+ @PathParam("fullName") String fullName,
+ @PathParam("tag") String tagName) {
+ LOG.info(
+ "Received get tag {} request for object type: {}, full name: {} under
metalake: {}",
+ tagName,
+ type,
+ fullName,
+ metalake);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ MetadataObject object =
+ MetadataObjects.parse(
+ fullName,
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+ Optional<Tag> tag = getTagForObject(metalake, object, tagName);
+ Optional<TagDTO> tagDTO = tag.map(t -> DTOConverters.toDTO(t,
Optional.of(false)));
+
+ MetadataObject parentObject = MetadataObjects.parent(object);
+ while (!tag.isPresent() && parentObject != null) {
+ tag = getTagForObject(metalake, parentObject, tagName);
+ tagDTO = tag.map(t -> DTOConverters.toDTO(t, Optional.of(true)));
+ parentObject = MetadataObjects.parent(parentObject);
+ }
+
+ if (!tagDTO.isPresent()) {
+ LOG.warn(
+ "Tag {} not found for object type: {}, full name: {} under
metalake: {}",
+ tagName,
+ type,
+ fullName,
+ metalake);
+ return Utils.notFound(
+ NoSuchTagException.class.getSimpleName(),
+ "Tag not found: "
+ + tagName
+ + " for object type: "
+ + type
+ + ", full name: "
+ + fullName
+ + " under metalake: "
+ + metalake);
+ } else {
+ LOG.info(
+ "Get tag: {} for object type: {}, full name: {} under
metalake: {}",
+ tagName,
+ type,
+ fullName,
+ metalake);
+ return Utils.ok(new TagResponse(tagDTO.get()));
+ }
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleTagException(OperationType.GET, tagName,
fullName, e);
+ }
+ }
+
+ @GET
+ @Path("{tag}/objects")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "list-objects-for-tag." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "list-objects-for-tag", absolute = true)
+ public Response listMetadataObjectsForTag(
+ @PathParam("metalake") String metalake, @PathParam("tag") String
tagName) {
+ LOG.info("Received list objects for tag: {} under metalake: {}", tagName,
metalake);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ MetadataObject[] objects =
tagManager.listMetadataObjectsForTag(metalake, tagName);
+ objects = objects == null ? new MetadataObject[0] : objects;
+
+ LOG.info(
+ "List {} objects for tag: {} under metalake: {}",
+ objects.length,
+ tagName,
+ metalake);
+
+ MetadataObjectDTO[] objectDTOs =
+
Arrays.stream(objects).map(DTOConverters::toDTO).toArray(MetadataObjectDTO[]::new);
+ return Utils.ok(new MetadataObjectListResponse(objectDTOs));
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleTagException(OperationType.LIST, "",
tagName, e);
+ }
+ }
+
+ @POST
+ @Path("{type}/{fullName}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "associate-object-tags." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "associate-object-tags", absolute = true)
+ public Response associateTagsForObject(
+ @PathParam("metalake") String metalake,
+ @PathParam("type") String type,
+ @PathParam("fullName") String fullName,
+ TagsAssociateRequest request) {
+ LOG.info(
+ "Received associate tags request for object type: {}, full name: {}
under metalake: {}",
+ type,
+ fullName,
+ metalake);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ request.validate();
+ MetadataObject object =
+ MetadataObjects.parse(
+ fullName,
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+ String[] tagNames =
+ tagManager.associateTagsForMetadataObject(
+ metalake, object, request.getTagsToAdd(),
request.getTagsToRemove());
+ tagNames = tagNames == null ? new String[0] : tagNames;
+
+ LOG.info(
+ "Associated tags: {} for object type: {}, full name: {} under
metalake: {}",
+ Arrays.toString(tagNames),
+ type,
+ fullName,
+ metalake);
+ return Utils.ok(new NameListResponse(tagNames));
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleTagException(OperationType.ASSOCIATE, "",
fullName, e);
+ }
+ }
+
+ private Optional<Tag> getTagForObject(String metalake, MetadataObject
object, String tagName) {
+ try {
+ return Optional.ofNullable(tagManager.getTagForMetadataObject(metalake,
object, tagName));
+ } catch (NoSuchTagException e) {
+ LOG.info("Tag {} not found for object: {}", tagName, object);
+ return Optional.empty();
+ }
+ }
+}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
new file mode 100644
index 000000000..23b87b60d
--- /dev/null
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestTagOperations.java
@@ -0,0 +1,1099 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.server.web.rest;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.dto.requests.TagCreateRequest;
+import org.apache.gravitino.dto.requests.TagUpdateRequest;
+import org.apache.gravitino.dto.requests.TagUpdatesRequest;
+import org.apache.gravitino.dto.requests.TagsAssociateRequest;
+import org.apache.gravitino.dto.responses.DropResponse;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.dto.responses.MetadataObjectListResponse;
+import org.apache.gravitino.dto.responses.NameListResponse;
+import org.apache.gravitino.dto.responses.TagListResponse;
+import org.apache.gravitino.dto.responses.TagResponse;
+import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.exceptions.TagAlreadyAssociatedException;
+import org.apache.gravitino.exceptions.TagAlreadyExistsException;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.TagEntity;
+import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagChange;
+import org.apache.gravitino.tag.TagManager;
+import org.glassfish.jersey.internal.inject.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagOperations extends JerseyTest {
+
+ private static class MockServletRequestFactory extends
ServletRequestFactoryBase {
+
+ @Override
+ public HttpServletRequest get() {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getRemoteUser()).thenReturn(null);
+ return request;
+ }
+ }
+
+ private TagManager tagManager = mock(TagManager.class);
+
+ private String metalake = "test_metalake";
+
+ private AuditInfo testAuditInfo1 =
+
AuditInfo.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+
+ @Override
+ protected Application configure() {
+ try {
+ forceSet(
+ TestProperties.CONTAINER_PORT,
String.valueOf(RESTUtils.findAvailablePort(2000, 3000)));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ ResourceConfig resourceConfig = new ResourceConfig();
+ resourceConfig.register(TagOperations.class);
+ resourceConfig.register(
+ new AbstractBinder() {
+ @Override
+ protected void configure() {
+ bind(tagManager).to(TagManager.class).ranked(2);
+ bindFactory(TestTagOperations.MockServletRequestFactory.class)
+ .to(HttpServletRequest.class);
+ }
+ });
+
+ return resourceConfig;
+ }
+
+ @Test
+ public void testListTags() {
+ String[] tags = new String[] {"tag1", "tag2"};
+ when(tagManager.listTags(metalake)).thenReturn(tags);
+
+ Response response =
+ target(tagPath(metalake))
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
response.getMediaType());
+
+ NameListResponse nameListResponse =
response.readEntity(NameListResponse.class);
+ Assertions.assertEquals(0, nameListResponse.getCode());
+ Assertions.assertArrayEquals(tags, nameListResponse.getNames());
+
+ when(tagManager.listTags(metalake)).thenReturn(null);
+ Response resp1 =
+ target(tagPath(metalake))
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp1.getStatus());
+
+ NameListResponse nameListResponse1 =
resp1.readEntity(NameListResponse.class);
+ Assertions.assertEquals(0, nameListResponse1.getCode());
+ Assertions.assertEquals(0, nameListResponse1.getNames().length);
+
+ when(tagManager.listTags(metalake)).thenReturn(new String[0]);
+ Response resp2 =
+ target(tagPath(metalake))
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp2.getStatus());
+
+ NameListResponse nameListResponse2 =
resp2.readEntity(NameListResponse.class);
+ Assertions.assertEquals(0, nameListResponse2.getCode());
+ Assertions.assertEquals(0, nameListResponse2.getNames().length);
+
+ // Test throw NoSuchMetalakeException
+ doThrow(new NoSuchMetalakeException("mock
error")).when(tagManager).listTags(metalake);
+ Response resp3 =
+ target(tagPath(metalake))
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
resp3.getStatus());
+
+ ErrorResponse errorResp = resp3.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResp.getCode());
+ Assertions.assertEquals(NoSuchMetalakeException.class.getSimpleName(),
errorResp.getType());
+
+ // Test throw RuntimeException
+ doThrow(new RuntimeException("mock
error")).when(tagManager).listTags(metalake);
+ Response resp4 =
+ target(tagPath(metalake))
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp4.getStatus());
+
+ ErrorResponse errorResp1 = resp4.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp1.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp1.getType());
+ }
+
+ @Test
+ public void testListTagsInfo() {
+ TagEntity tag1 =
+ TagEntity.builder()
+ .withName("tag1")
+ .withId(1L)
+ .withComment("tag1 comment")
+ .withAuditInfo(testAuditInfo1)
+ .build();
+
+ TagEntity tag2 =
+ TagEntity.builder()
+ .withName("tag2")
+ .withId(1L)
+ .withComment("tag2 comment")
+ .withAuditInfo(testAuditInfo1)
+ .build();
+
+ Tag[] tags = new Tag[] {tag1, tag2};
+ when(tagManager.listTagsInfo(metalake)).thenReturn(tags);
+
+ Response resp =
+ target(tagPath(metalake))
+ .queryParam("details", true)
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ TagListResponse tagListResp = resp.readEntity(TagListResponse.class);
+ Assertions.assertEquals(0, tagListResp.getCode());
+ Assertions.assertEquals(tags.length, tagListResp.getTags().length);
+
+ Assertions.assertEquals(tag1.name(), tagListResp.getTags()[0].name());
+ Assertions.assertEquals(tag1.comment(),
tagListResp.getTags()[0].comment());
+ Assertions.assertEquals(Optional.empty(),
tagListResp.getTags()[0].inherited());
+
+ Assertions.assertEquals(tag2.name(), tagListResp.getTags()[1].name());
+ Assertions.assertEquals(tag2.comment(),
tagListResp.getTags()[1].comment());
+ Assertions.assertEquals(Optional.empty(),
tagListResp.getTags()[1].inherited());
+
+ // Test return null
+ when(tagManager.listTagsInfo(metalake)).thenReturn(null);
+ Response resp1 =
+ target(tagPath(metalake))
+ .queryParam("details", true)
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp1.getStatus());
+
+ TagListResponse tagListResp1 = resp1.readEntity(TagListResponse.class);
+ Assertions.assertEquals(0, tagListResp1.getCode());
+ Assertions.assertEquals(0, tagListResp1.getTags().length);
+
+ // Test return empty array
+ when(tagManager.listTagsInfo(metalake)).thenReturn(new Tag[0]);
+ Response resp2 =
+ target(tagPath(metalake))
+ .queryParam("details", true)
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp2.getStatus());
+
+ TagListResponse tagListResp2 = resp2.readEntity(TagListResponse.class);
+ Assertions.assertEquals(0, tagListResp2.getCode());
+ Assertions.assertEquals(0, tagListResp2.getTags().length);
+ }
+
+ @Test
+ public void testCreateTag() {
+ TagEntity tag1 =
+ TagEntity.builder()
+ .withName("tag1")
+ .withId(1L)
+ .withComment("tag1 comment")
+ .withAuditInfo(testAuditInfo1)
+ .build();
+ when(tagManager.createTag(metalake, "tag1", "tag1 comment",
null)).thenReturn(tag1);
+
+ TagCreateRequest request = new TagCreateRequest("tag1", "tag1 comment",
null);
+ Response resp =
+ target(tagPath(metalake))
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ TagResponse tagResp = resp.readEntity(TagResponse.class);
+ Assertions.assertEquals(0, tagResp.getCode());
+
+ Tag respTag = tagResp.getTag();
+ Assertions.assertEquals(tag1.name(), respTag.name());
+ Assertions.assertEquals(tag1.comment(), respTag.comment());
+ Assertions.assertEquals(Optional.empty(), respTag.inherited());
+
+ // Test throw TagAlreadyExistsException
+ doThrow(new TagAlreadyExistsException("mock error"))
+ .when(tagManager)
+ .createTag(any(), any(), any(), any());
+ Response resp1 =
+ target(tagPath(metalake))
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(),
resp1.getStatus());
+
+ ErrorResponse errorResp = resp1.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.ALREADY_EXISTS_CODE,
errorResp.getCode());
+ Assertions.assertEquals(TagAlreadyExistsException.class.getSimpleName(),
errorResp.getType());
+
+ // Test throw RuntimeException
+ doThrow(new RuntimeException("mock error"))
+ .when(tagManager)
+ .createTag(any(), any(), any(), any());
+
+ Response resp2 =
+ target(tagPath(metalake))
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp2.getStatus());
+
+ ErrorResponse errorResp1 = resp2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp1.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp1.getType());
+ }
+
+ @Test
+ public void testGetTag() {
+ TagEntity tag1 =
+ TagEntity.builder()
+ .withName("tag1")
+ .withId(1L)
+ .withComment("tag1 comment")
+ .withAuditInfo(testAuditInfo1)
+ .build();
+ when(tagManager.getTag(metalake, "tag1")).thenReturn(tag1);
+
+ Response resp =
+ target(tagPath(metalake))
+ .path("tag1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ TagResponse tagResp = resp.readEntity(TagResponse.class);
+ Assertions.assertEquals(0, tagResp.getCode());
+
+ Tag respTag = tagResp.getTag();
+ Assertions.assertEquals(tag1.name(), respTag.name());
+ Assertions.assertEquals(tag1.comment(), respTag.comment());
+ Assertions.assertEquals(Optional.empty(), respTag.inherited());
+
+ // Test throw NoSuchTagException
+ doThrow(new NoSuchTagException("mock
error")).when(tagManager).getTag(metalake, "tag1");
+
+ Response resp2 =
+ target(tagPath(metalake))
+ .path("tag1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
resp2.getStatus());
+
+ ErrorResponse errorResp = resp2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResp.getCode());
+ Assertions.assertEquals(NoSuchTagException.class.getSimpleName(),
errorResp.getType());
+
+ // Test throw RuntimeException
+ doThrow(new RuntimeException("mock
error")).when(tagManager).getTag(metalake, "tag1");
+
+ Response resp3 =
+ target(tagPath(metalake))
+ .path("tag1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp3.getStatus());
+
+ ErrorResponse errorResp1 = resp3.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp1.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp1.getType());
+ }
+
+ @Test
+ public void testAlterTag() {
+ TagEntity newTag =
+ TagEntity.builder()
+ .withName("new_tag1")
+ .withId(1L)
+ .withComment("new tag1 comment")
+ .withAuditInfo(testAuditInfo1)
+ .build();
+
+ TagChange[] changes =
+ new TagChange[] {TagChange.rename("new_tag1"),
TagChange.updateComment("new tag1 comment")};
+
+ when(tagManager.alterTag(metalake, "tag1", changes)).thenReturn(newTag);
+
+ TagUpdateRequest[] requests =
+ new TagUpdateRequest[] {
+ new TagUpdateRequest.RenameTagRequest("new_tag1"),
+ new TagUpdateRequest.UpdateTagCommentRequest("new tag1 comment")
+ };
+ TagUpdatesRequest request = new
TagUpdatesRequest(Lists.newArrayList(requests));
+ Response resp =
+ target(tagPath(metalake))
+ .path("tag1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ TagResponse tagResp = resp.readEntity(TagResponse.class);
+ Assertions.assertEquals(0, tagResp.getCode());
+
+ Tag respTag = tagResp.getTag();
+ Assertions.assertEquals(newTag.name(), respTag.name());
+ Assertions.assertEquals(newTag.comment(), respTag.comment());
+ Assertions.assertEquals(Optional.empty(), respTag.inherited());
+
+ // Test throw NoSuchTagException
+ doThrow(new NoSuchTagException("mock
error")).when(tagManager).alterTag(any(), any(), any());
+
+ Response resp1 =
+ target(tagPath(metalake))
+ .path("tag1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
resp1.getStatus());
+
+ ErrorResponse errorResp = resp1.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResp.getCode());
+ Assertions.assertEquals(NoSuchTagException.class.getSimpleName(),
errorResp.getType());
+
+ // Test throw RuntimeException
+ doThrow(new RuntimeException("mock
error")).when(tagManager).alterTag(any(), any(), any());
+
+ Response resp2 =
+ target(tagPath(metalake))
+ .path("tag1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp2.getStatus());
+
+ ErrorResponse errorResp1 = resp2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp1.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp1.getType());
+ }
+
+ @Test
+ public void testDeleteTag() {
+ when(tagManager.deleteTag(metalake, "tag1")).thenReturn(true);
+
+ Response resp =
+ target(tagPath(metalake))
+ .path("tag1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .delete();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ DropResponse dropResp = resp.readEntity(DropResponse.class);
+ Assertions.assertEquals(0, dropResp.getCode());
+ Assertions.assertTrue(dropResp.dropped());
+
+ when(tagManager.deleteTag(metalake, "tag1")).thenReturn(false);
+ Response resp1 =
+ target(tagPath(metalake))
+ .path("tag1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .delete();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp1.getStatus());
+
+ DropResponse dropResp1 = resp1.readEntity(DropResponse.class);
+ Assertions.assertEquals(0, dropResp1.getCode());
+ Assertions.assertFalse(dropResp1.dropped());
+
+ // Test throw RuntimeException
+ doThrow(new RuntimeException("mock
error")).when(tagManager).deleteTag(any(), any());
+
+ Response resp2 =
+ target(tagPath(metalake))
+ .path("tag1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .delete();
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp2.getStatus());
+
+ ErrorResponse errorResp1 = resp2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp1.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp1.getType());
+ }
+
+ @Test
+ public void testListTagsForObject() {
+ MetadataObject catalog = MetadataObjects.parse("object1",
MetadataObject.Type.CATALOG);
+ MetadataObject schema = MetadataObjects.parse("object1.object2",
MetadataObject.Type.SCHEMA);
+ MetadataObject table =
+ MetadataObjects.parse("object1.object2.object3",
MetadataObject.Type.TABLE);
+ MetadataObject column =
+ MetadataObjects.parse("object1.object2.object3.object4",
MetadataObject.Type.COLUMN);
+
+ Tag[] catalogTagInfos =
+ new Tag[] {
+
TagEntity.builder().withName("tag1").withId(1L).withAuditInfo(testAuditInfo1).build()
+ };
+ when(tagManager.listTagsInfoForMetadataObject(metalake,
catalog)).thenReturn(catalogTagInfos);
+
+ Tag[] schemaTagInfos =
+ new Tag[] {
+
TagEntity.builder().withName("tag3").withId(1L).withAuditInfo(testAuditInfo1).build()
+ };
+ when(tagManager.listTagsInfoForMetadataObject(metalake,
schema)).thenReturn(schemaTagInfos);
+
+ Tag[] tableTagInfos =
+ new Tag[] {
+
TagEntity.builder().withName("tag5").withId(1L).withAuditInfo(testAuditInfo1).build()
+ };
+ when(tagManager.listTagsInfoForMetadataObject(metalake,
table)).thenReturn(tableTagInfos);
+
+ Tag[] columnTagInfos =
+ new Tag[] {
+
TagEntity.builder().withName("tag7").withId(1L).withAuditInfo(testAuditInfo1).build()
+ };
+ when(tagManager.listTagsInfoForMetadataObject(metalake,
column)).thenReturn(columnTagInfos);
+
+ // Test catalog tags
+ Response response =
+ target(tagPath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .queryParam("details", true)
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
+
+ TagListResponse tagListResponse =
response.readEntity(TagListResponse.class);
+ Assertions.assertEquals(0, tagListResponse.getCode());
+ Assertions.assertEquals(catalogTagInfos.length,
tagListResponse.getTags().length);
+
+ Map<String, Tag> resultTags =
+ Arrays.stream(tagListResponse.getTags())
+ .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+ Assertions.assertTrue(resultTags.containsKey("tag1"));
+ Assertions.assertFalse(resultTags.get("tag1").inherited().get());
+
+ Response response1 =
+ target(tagPath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response1.getStatus());
+
+ NameListResponse nameListResponse =
response1.readEntity(NameListResponse.class);
+ Assertions.assertEquals(0, nameListResponse.getCode());
+ Assertions.assertEquals(catalogTagInfos.length,
nameListResponse.getNames().length);
+ Assertions.assertArrayEquals(
+ Arrays.stream(catalogTagInfos).map(Tag::name).toArray(String[]::new),
+ nameListResponse.getNames());
+
+ // Test schema tags
+ Response response2 =
+ target(tagPath(metalake))
+ .path(schema.type().toString())
+ .path(schema.fullName())
+ .queryParam("details", true)
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response2.getStatus());
+
+ TagListResponse tagListResponse1 =
response2.readEntity(TagListResponse.class);
+ Assertions.assertEquals(0, tagListResponse1.getCode());
+ Assertions.assertEquals(
+ schemaTagInfos.length + catalogTagInfos.length,
tagListResponse1.getTags().length);
+
+ Map<String, Tag> resultTags1 =
+ Arrays.stream(tagListResponse1.getTags())
+ .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+ Assertions.assertTrue(resultTags1.containsKey("tag1"));
+ Assertions.assertTrue(resultTags1.containsKey("tag3"));
+
+ Assertions.assertTrue(resultTags1.get("tag1").inherited().get());
+ Assertions.assertFalse(resultTags1.get("tag3").inherited().get());
+
+ Response response3 =
+ target(tagPath(metalake))
+ .path(schema.type().toString())
+ .path(schema.fullName())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response3.getStatus());
+
+ NameListResponse nameListResponse1 =
response3.readEntity(NameListResponse.class);
+ Assertions.assertEquals(0, nameListResponse1.getCode());
+ Assertions.assertEquals(
+ schemaTagInfos.length + catalogTagInfos.length,
nameListResponse1.getNames().length);
+ Set<String> resultNames = Sets.newHashSet(nameListResponse1.getNames());
+ Assertions.assertTrue(resultNames.contains("tag1"));
+ Assertions.assertTrue(resultNames.contains("tag3"));
+
+ // Test table tags
+ Response response4 =
+ target(tagPath(metalake))
+ .path(table.type().toString())
+ .path(table.fullName())
+ .queryParam("details", true)
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response4.getStatus());
+
+ TagListResponse tagListResponse2 =
response4.readEntity(TagListResponse.class);
+ Assertions.assertEquals(0, tagListResponse2.getCode());
+ Assertions.assertEquals(
+ schemaTagInfos.length + catalogTagInfos.length + tableTagInfos.length,
+ tagListResponse2.getTags().length);
+
+ Map<String, Tag> resultTags2 =
+ Arrays.stream(tagListResponse2.getTags())
+ .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+ Assertions.assertTrue(resultTags2.containsKey("tag1"));
+ Assertions.assertTrue(resultTags2.containsKey("tag3"));
+ Assertions.assertTrue(resultTags2.containsKey("tag5"));
+
+ Assertions.assertTrue(resultTags2.get("tag1").inherited().get());
+ Assertions.assertTrue(resultTags2.get("tag3").inherited().get());
+ Assertions.assertFalse(resultTags2.get("tag5").inherited().get());
+
+ Response response5 =
+ target(tagPath(metalake))
+ .path(table.type().toString())
+ .path(table.fullName())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response5.getStatus());
+
+ NameListResponse nameListResponse2 =
response5.readEntity(NameListResponse.class);
+ Assertions.assertEquals(0, nameListResponse2.getCode());
+ Assertions.assertEquals(
+ schemaTagInfos.length + catalogTagInfos.length + tableTagInfos.length,
+ nameListResponse2.getNames().length);
+
+ Set<String> resultNames1 = Sets.newHashSet(nameListResponse2.getNames());
+ Assertions.assertTrue(resultNames1.contains("tag1"));
+ Assertions.assertTrue(resultNames1.contains("tag3"));
+ Assertions.assertTrue(resultNames1.contains("tag5"));
+
+ // Test column tags
+ Response response6 =
+ target(tagPath(metalake))
+ .path(column.type().toString())
+ .path(column.fullName())
+ .queryParam("details", true)
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response6.getStatus());
+
+ TagListResponse tagListResponse3 =
response6.readEntity(TagListResponse.class);
+ Assertions.assertEquals(0, tagListResponse3.getCode());
+ Assertions.assertEquals(
+ schemaTagInfos.length
+ + catalogTagInfos.length
+ + tableTagInfos.length
+ + columnTagInfos.length,
+ tagListResponse3.getTags().length);
+
+ Map<String, Tag> resultTags3 =
+ Arrays.stream(tagListResponse3.getTags())
+ .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+ Assertions.assertTrue(resultTags3.containsKey("tag1"));
+ Assertions.assertTrue(resultTags3.containsKey("tag3"));
+ Assertions.assertTrue(resultTags3.containsKey("tag5"));
+ Assertions.assertTrue(resultTags3.containsKey("tag7"));
+
+ Assertions.assertTrue(resultTags3.get("tag1").inherited().get());
+ Assertions.assertTrue(resultTags3.get("tag3").inherited().get());
+ Assertions.assertTrue(resultTags3.get("tag5").inherited().get());
+ Assertions.assertFalse(resultTags3.get("tag7").inherited().get());
+
+ Response response7 =
+ target(tagPath(metalake))
+ .path(column.type().toString())
+ .path(column.fullName())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response7.getStatus());
+
+ NameListResponse nameListResponse3 =
response7.readEntity(NameListResponse.class);
+ Assertions.assertEquals(0, nameListResponse3.getCode());
+
+ Assertions.assertEquals(
+ schemaTagInfos.length
+ + catalogTagInfos.length
+ + tableTagInfos.length
+ + columnTagInfos.length,
+ nameListResponse3.getNames().length);
+
+ Set<String> resultNames2 = Sets.newHashSet(nameListResponse3.getNames());
+ Assertions.assertTrue(resultNames2.contains("tag1"));
+ Assertions.assertTrue(resultNames2.contains("tag3"));
+ Assertions.assertTrue(resultNames2.contains("tag5"));
+ Assertions.assertTrue(resultNames2.contains("tag7"));
+ }
+
+ @Test
+ public void testGetTagForObject() {
+ TagEntity tag1 =
+
TagEntity.builder().withName("tag1").withId(1L).withAuditInfo(testAuditInfo1).build();
+ MetadataObject catalog = MetadataObjects.parse("object1",
MetadataObject.Type.CATALOG);
+ when(tagManager.getTagForMetadataObject(metalake, catalog,
"tag1")).thenReturn(tag1);
+
+ TagEntity tag2 =
+
TagEntity.builder().withName("tag2").withId(1L).withAuditInfo(testAuditInfo1).build();
+ MetadataObject schema = MetadataObjects.parse("object1.object2",
MetadataObject.Type.SCHEMA);
+ when(tagManager.getTagForMetadataObject(metalake, schema,
"tag2")).thenReturn(tag2);
+
+ TagEntity tag3 =
+
TagEntity.builder().withName("tag3").withId(1L).withAuditInfo(testAuditInfo1).build();
+ MetadataObject table =
+ MetadataObjects.parse("object1.object2.object3",
MetadataObject.Type.TABLE);
+ when(tagManager.getTagForMetadataObject(metalake, table,
"tag3")).thenReturn(tag3);
+
+ TagEntity tag4 =
+
TagEntity.builder().withName("tag4").withId(1L).withAuditInfo(testAuditInfo1).build();
+ MetadataObject column =
+ MetadataObjects.parse("object1.object2.object3.object4",
MetadataObject.Type.COLUMN);
+ when(tagManager.getTagForMetadataObject(metalake, column,
"tag4")).thenReturn(tag4);
+
+ // Test catalog tag
+ Response response =
+ target(tagPath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .path("tag1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
+
+ TagResponse tagResponse = response.readEntity(TagResponse.class);
+ Assertions.assertEquals(0, tagResponse.getCode());
+
+ Tag respTag = tagResponse.getTag();
+ Assertions.assertEquals(tag1.name(), respTag.name());
+ Assertions.assertEquals(tag1.comment(), respTag.comment());
+ Assertions.assertFalse(respTag.inherited().get());
+
+ // Test schema tag
+ Response response1 =
+ target(tagPath(metalake))
+ .path(schema.type().toString())
+ .path(schema.fullName())
+ .path("tag2")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response1.getStatus());
+
+ TagResponse tagResponse1 = response1.readEntity(TagResponse.class);
+ Assertions.assertEquals(0, tagResponse1.getCode());
+
+ Tag respTag1 = tagResponse1.getTag();
+ Assertions.assertEquals(tag2.name(), respTag1.name());
+ Assertions.assertEquals(tag2.comment(), respTag1.comment());
+ Assertions.assertFalse(respTag1.inherited().get());
+
+ // Test table tag
+ Response response2 =
+ target(tagPath(metalake))
+ .path(table.type().toString())
+ .path(table.fullName())
+ .path("tag3")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response2.getStatus());
+
+ TagResponse tagResponse2 = response2.readEntity(TagResponse.class);
+ Assertions.assertEquals(0, tagResponse2.getCode());
+
+ Tag respTag2 = tagResponse2.getTag();
+ Assertions.assertEquals(tag3.name(), respTag2.name());
+ Assertions.assertEquals(tag3.comment(), respTag2.comment());
+ Assertions.assertFalse(respTag2.inherited().get());
+
+ // Test column tag
+ Response response3 =
+ target(tagPath(metalake))
+ .path(column.type().toString())
+ .path(column.fullName())
+ .path("tag4")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response3.getStatus());
+
+ TagResponse tagResponse3 = response3.readEntity(TagResponse.class);
+ Assertions.assertEquals(0, tagResponse3.getCode());
+
+ Tag respTag3 = tagResponse3.getTag();
+ Assertions.assertEquals(tag4.name(), respTag3.name());
+ Assertions.assertEquals(tag4.comment(), respTag3.comment());
+ Assertions.assertFalse(respTag3.inherited().get());
+
+ // Test get schema inherited tag
+ Response response4 =
+ target(tagPath(metalake))
+ .path(schema.type().toString())
+ .path(schema.fullName())
+ .path("tag1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response4.getStatus());
+
+ TagResponse tagResponse4 = response4.readEntity(TagResponse.class);
+ Assertions.assertEquals(0, tagResponse4.getCode());
+
+ Tag respTag4 = tagResponse4.getTag();
+ Assertions.assertEquals(tag1.name(), respTag4.name());
+ Assertions.assertEquals(tag1.comment(), respTag4.comment());
+ Assertions.assertTrue(respTag4.inherited().get());
+
+ // Test get table inherited tag
+ Response response5 =
+ target(tagPath(metalake))
+ .path(table.type().toString())
+ .path(table.fullName())
+ .path("tag2")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response5.getStatus());
+
+ TagResponse tagResponse5 = response5.readEntity(TagResponse.class);
+ Assertions.assertEquals(0, tagResponse5.getCode());
+
+ Tag respTag5 = tagResponse5.getTag();
+ Assertions.assertEquals(tag2.name(), respTag5.name());
+ Assertions.assertEquals(tag2.comment(), respTag5.comment());
+ Assertions.assertTrue(respTag5.inherited().get());
+
+ // Test get column inherited tag
+ Response response6 =
+ target(tagPath(metalake))
+ .path(column.type().toString())
+ .path(column.fullName())
+ .path("tag3")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response6.getStatus());
+
+ TagResponse tagResponse6 = response6.readEntity(TagResponse.class);
+ Assertions.assertEquals(0, tagResponse6.getCode());
+
+ Tag respTag6 = tagResponse6.getTag();
+ Assertions.assertEquals(tag3.name(), respTag6.name());
+ Assertions.assertEquals(tag3.comment(), respTag6.comment());
+ Assertions.assertTrue(respTag6.inherited().get());
+
+ // Test catalog tag throw NoSuchTagException
+ Response response7 =
+ target(tagPath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .path("tag2")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
response7.getStatus());
+
+ ErrorResponse errorResponse = response7.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResponse.getCode());
+ Assertions.assertEquals(NoSuchTagException.class.getSimpleName(),
errorResponse.getType());
+
+ // Test schema tag throw NoSuchTagException
+ Response response8 =
+ target(tagPath(metalake))
+ .path(schema.type().toString())
+ .path(schema.fullName())
+ .path("tag3")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
response8.getStatus());
+
+ ErrorResponse errorResponse1 = response8.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResponse1.getCode());
+ Assertions.assertEquals(NoSuchTagException.class.getSimpleName(),
errorResponse1.getType());
+ }
+
+ @Test
+ public void testAssociateTagsForObject() {
+ String[] tagsToAdd = new String[] {"tag1", "tag2"};
+ String[] tagsToRemove = new String[] {"tag3", "tag4"};
+
+ MetadataObject catalog = MetadataObjects.parse("object1",
MetadataObject.Type.CATALOG);
+ when(tagManager.associateTagsForMetadataObject(metalake, catalog,
tagsToAdd, tagsToRemove))
+ .thenReturn(tagsToAdd);
+
+ TagsAssociateRequest request = new TagsAssociateRequest(tagsToAdd,
tagsToRemove);
+ Response response =
+ target(tagPath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
response.getMediaType());
+
+ NameListResponse nameListResponse =
response.readEntity(NameListResponse.class);
+ Assertions.assertEquals(0, nameListResponse.getCode());
+
+ Assertions.assertArrayEquals(tagsToAdd, nameListResponse.getNames());
+
+ // Test throw null tags
+ when(tagManager.associateTagsForMetadataObject(metalake, catalog,
tagsToAdd, tagsToRemove))
+ .thenReturn(null);
+ Response response1 =
+ target(tagPath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response1.getStatus());
+
+ NameListResponse nameListResponse1 =
response1.readEntity(NameListResponse.class);
+ Assertions.assertEquals(0, nameListResponse1.getCode());
+
+ Assertions.assertEquals(0, nameListResponse1.getNames().length);
+
+ // Test throw TagAlreadyAssociatedException
+ doThrow(new TagAlreadyAssociatedException("mock error"))
+ .when(tagManager)
+ .associateTagsForMetadataObject(metalake, catalog, tagsToAdd,
tagsToRemove);
+ Response response2 =
+ target(tagPath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(),
response2.getStatus());
+
+ ErrorResponse errorResponse = response2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.ALREADY_EXISTS_CODE,
errorResponse.getCode());
+ Assertions.assertEquals(
+ TagAlreadyAssociatedException.class.getSimpleName(),
errorResponse.getType());
+
+ // Test throw RuntimeException
+ doThrow(new RuntimeException("mock error"))
+ .when(tagManager)
+ .associateTagsForMetadataObject(any(), any(), any(), any());
+
+ Response response3 =
+ target(tagPath(metalake))
+ .path(catalog.type().toString())
+ .path(catalog.fullName())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
response3.getStatus());
+
+ ErrorResponse errorResponse1 = response3.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResponse1.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResponse1.getType());
+ }
+
+ @Test
+ public void testListMetadataObjectForTag() {
+ MetadataObject[] objects =
+ new MetadataObject[] {
+ MetadataObjects.parse("object1", MetadataObject.Type.CATALOG),
+ MetadataObjects.parse("object1.object2", MetadataObject.Type.SCHEMA),
+ MetadataObjects.parse("object1.object2.object3",
MetadataObject.Type.TABLE),
+ MetadataObjects.parse("object1.object2.object3.object4",
MetadataObject.Type.COLUMN)
+ };
+
+ when(tagManager.listMetadataObjectsForTag(metalake,
"tag1")).thenReturn(objects);
+
+ Response response =
+ target(tagPath(metalake))
+ .path("tag1")
+ .path("objects")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
response.getMediaType());
+
+ MetadataObjectListResponse objectListResponse =
+ response.readEntity(MetadataObjectListResponse.class);
+ Assertions.assertEquals(0, objectListResponse.getCode());
+
+ MetadataObject[] respObjects = objectListResponse.getMetadataObjects();
+ Assertions.assertEquals(objects.length, respObjects.length);
+
+ for (int i = 0; i < objects.length; i++) {
+ Assertions.assertEquals(objects[i].type(), respObjects[i].type());
+ Assertions.assertEquals(objects[i].fullName(),
respObjects[i].fullName());
+ }
+
+ // Test throw NoSuchTagException
+ doThrow(new NoSuchTagException("mock error"))
+ .when(tagManager)
+ .listMetadataObjectsForTag(metalake, "tag1");
+
+ Response response1 =
+ target(tagPath(metalake))
+ .path("tag1")
+ .path("objects")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
response1.getStatus());
+
+ ErrorResponse errorResponse = response1.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResponse.getCode());
+ Assertions.assertEquals(NoSuchTagException.class.getSimpleName(),
errorResponse.getType());
+
+ // Test throw RuntimeException
+ doThrow(new RuntimeException("mock error"))
+ .when(tagManager)
+ .listMetadataObjectsForTag(any(), any());
+
+ Response response2 =
+ target(tagPath(metalake))
+ .path("tag1")
+ .path("objects")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
response2.getStatus());
+
+ ErrorResponse errorResponse1 = response2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResponse1.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResponse1.getType());
+ }
+
+ private String tagPath(String metalake) {
+ return "/metalakes/" + metalake + "/tags";
+ }
+}