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 <jerrys...@datastrato.com> 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 <67318028+bkn...@users.noreply.github.com> Co-authored-by: Dev Parikh <51128342+dev79...@users.noreply.github.com> Co-authored-by: roryqi <ror...@apache.org> Co-authored-by: JinsYin <jins...@163.com> Co-authored-by: rqyin <rq...@easipass.com> --- .../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"; + } +}