This is an automated email from the ASF dual-hosted git repository.
mchades 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 a68e5e22ad [#5817] core(feat): Add server-side REST APIs for model
management (#5948)
a68e5e22ad is described below
commit a68e5e22addb28448b91ddeb717d52bf7d9f74e0
Author: Jerry Shao <[email protected]>
AuthorDate: Thu Dec 26 15:30:55 2024 +0800
[#5817] core(feat): Add server-side REST APIs for model management (#5948)
### What changes were proposed in this pull request?
This PR adds the server-side REST endpoint for model management.
### Why are the changes needed?
This is a part of model management for Gravitino.
Fix: #5817
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Add UTs for this PR.
---
.../org/apache/gravitino/model/ModelCatalog.java | 8 +-
.../gravitino/catalog/model/ModelCatalogImpl.java | 3 +-
.../services/org.apache.gravitino.CatalogProvider | 2 +-
.../org/apache/gravitino/dto/model/ModelDTO.java | 163 ++++
.../gravitino/dto/model/ModelVersionDTO.java | 184 +++++
.../dto/requests/CatalogCreateRequest.java | 31 +-
.../dto/requests/ModelRegisterRequest.java | 54 ++
.../dto/requests/ModelVersionLinkRequest.java | 67 ++
.../gravitino/dto/responses/ModelResponse.java | 59 ++
.../dto/responses/ModelVersionListResponse.java | 57 ++
.../dto/responses/ModelVersionResponse.java | 59 ++
.../apache/gravitino/dto/util/DTOConverters.java | 37 +
.../apache/gravitino/dto/model/TestModelDTO.java | 81 ++
.../gravitino/dto/model/TestModelVersionDTO.java | 133 ++++
.../dto/requests/TestCatalogCreateRequest.java | 15 +-
.../dto/requests/TestModelRegisterRequest.java | 47 ++
.../dto/requests/TestModelVersionLinkRequest.java | 75 ++
.../gravitino/dto/responses/TestResponses.java | 76 ++
docs/open-api/models.yaml | 561 ++++++++++++++
docs/open-api/openapi.yaml | 23 +
.../apache/gravitino/server/GravitinoServer.java | 2 +
.../server/web/rest/ExceptionHandlers.java | 45 ++
.../gravitino/server/web/rest/ModelOperations.java | 411 ++++++++++
.../gravitino/server/web/rest/OperationType.java | 3 +
.../server/web/rest/TestModelOperations.java | 843 +++++++++++++++++++++
25 files changed, 3006 insertions(+), 33 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/model/ModelCatalog.java
b/api/src/main/java/org/apache/gravitino/model/ModelCatalog.java
index cea2e94e3c..3fb39c18ae 100644
--- a/api/src/main/java/org/apache/gravitino/model/ModelCatalog.java
+++ b/api/src/main/java/org/apache/gravitino/model/ModelCatalog.java
@@ -93,7 +93,10 @@ public interface ModelCatalog {
*
* @param ident The name identifier of the model.
* @param uri The model artifact URI.
- * @param aliases The aliases of the model version. The alias are optional
and can be empty.
+ * @param aliases The aliases of the model version. The aliases should be
unique in this model,
+ * otherwise the {@link ModelVersionAliasesAlreadyExistException} will
be thrown. The aliases
+ * are optional and can be empty. Also, be aware that the alias cannot
be a number or a number
+ * string.
* @param comment The comment of the model. The comment is optional and can
be null.
* @param properties The properties of the model. The properties are
optional and can be null or
* empty.
@@ -198,7 +201,8 @@ public interface ModelCatalog {
* @param uri The URI of the model version artifact.
* @param aliases The aliases of the model version. The aliases should be
unique in this model,
* otherwise the {@link ModelVersionAliasesAlreadyExistException} will
be thrown. The aliases
- * are optional and can be empty.
+ * are optional and can be empty. Also, be aware that the alias cannot
be a number or a number
+ * string.
* @param comment The comment of the model version. The comment is optional
and can be null.
* @param properties The properties of the model version. The properties are
optional and can be
* null or empty.
diff --git
a/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
b/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
index 5b90eab726..545f6482a3 100644
---
a/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
+++
b/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogImpl.java
@@ -19,7 +19,6 @@
package org.apache.gravitino.catalog.model;
import java.util.Map;
-import org.apache.gravitino.CatalogProvider;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.connector.BaseCatalog;
@@ -40,7 +39,7 @@ public class ModelCatalogImpl extends
BaseCatalog<ModelCatalogImpl> {
@Override
public String shortName() {
- return CatalogProvider.shortNameForManagedCatalog(super.type());
+ return "model";
}
@Override
diff --git
a/catalogs/catalog-model/src/main/resources/META-INF/services/org.apache.gravitino.CatalogProvider
b/catalogs/catalog-model/src/main/resources/META-INF/services/org.apache.gravitino.CatalogProvider
index 37c682aa74..e43f995ea7 100644
---
a/catalogs/catalog-model/src/main/resources/META-INF/services/org.apache.gravitino.CatalogProvider
+++
b/catalogs/catalog-model/src/main/resources/META-INF/services/org.apache.gravitino.CatalogProvider
@@ -16,4 +16,4 @@
# specific language governing permissions and limitations
# under the License.
#
-org.apache.gravitino.catalog.model.ModelCatalog
+org.apache.gravitino.catalog.model.ModelCatalogImpl
diff --git a/common/src/main/java/org/apache/gravitino/dto/model/ModelDTO.java
b/common/src/main/java/org/apache/gravitino/dto/model/ModelDTO.java
new file mode 100644
index 0000000000..4468839933
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/model/ModelDTO.java
@@ -0,0 +1,163 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.model.Model;
+
+/** Represents a model DTO (Data Transfer Object). */
+@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@EqualsAndHashCode
+public class ModelDTO implements Model {
+
+ @JsonProperty("name")
+ private String name;
+
+ @JsonProperty("comment")
+ private String comment;
+
+ @JsonProperty("properties")
+ private Map<String, String> properties;
+
+ @JsonProperty("latestVersion")
+ private int latestVersion;
+
+ @JsonProperty("audit")
+ private AuditDTO audit;
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public String comment() {
+ return comment;
+ }
+
+ @Override
+ public Map<String, String> properties() {
+ return properties;
+ }
+
+ @Override
+ public int latestVersion() {
+ return latestVersion;
+ }
+
+ @Override
+ public AuditDTO auditInfo() {
+ return audit;
+ }
+
+ /**
+ * Creates a new builder for constructing a Model DTO.
+ *
+ * @return The builder.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Builder for constructing a Model DTO. */
+ public static class Builder {
+ private String name;
+ private String comment;
+ private Map<String, String> properties;
+ private int latestVersion;
+ private AuditDTO audit;
+
+ /**
+ * Sets the name of the model.
+ *
+ * @param name The name of the model.
+ * @return The builder.
+ */
+ public Builder withName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Sets the comment associated with the model.
+ *
+ * @param comment The comment associated with the model.
+ * @return The builder.
+ */
+ public Builder withComment(String comment) {
+ this.comment = comment;
+ return this;
+ }
+
+ /**
+ * Sets the properties associated with the model.
+ *
+ * @param properties The properties associated with the model.
+ * @return The builder.
+ */
+ public Builder withProperties(Map<String, String> properties) {
+ this.properties = properties;
+ return this;
+ }
+
+ /**
+ * Sets the latest version of the model.
+ *
+ * @param latestVersion The latest version of the model.
+ * @return The builder.
+ */
+ public Builder withLatestVersion(int latestVersion) {
+ this.latestVersion = latestVersion;
+ return this;
+ }
+
+ /**
+ * Sets the audit information associated with the model.
+ *
+ * @param audit The audit information associated with the model.
+ * @return The builder.
+ */
+ public Builder withAudit(AuditDTO audit) {
+ this.audit = audit;
+ return this;
+ }
+
+ /**
+ * Builds the model DTO.
+ *
+ * @return The model DTO.
+ */
+ public ModelDTO build() {
+ Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot
be null or empty");
+ Preconditions.checkArgument(latestVersion >= 0, "latestVersion cannot be
negative");
+ Preconditions.checkArgument(audit != null, "audit cannot be null");
+
+ return new ModelDTO(name, comment, properties, latestVersion, audit);
+ }
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/model/ModelVersionDTO.java
b/common/src/main/java/org/apache/gravitino/dto/model/ModelVersionDTO.java
new file mode 100644
index 0000000000..e887ba5bdb
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/model/ModelVersionDTO.java
@@ -0,0 +1,184 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.model.ModelVersion;
+
+/** Represents a model version DTO (Data Transfer Object). */
+@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@EqualsAndHashCode
+public class ModelVersionDTO implements ModelVersion {
+
+ @JsonProperty("version")
+ private int version;
+
+ @JsonProperty("comment")
+ private String comment;
+
+ @JsonProperty("aliases")
+ private String[] aliases;
+
+ @JsonProperty("uri")
+ private String uri;
+
+ @JsonProperty("properties")
+ private Map<String, String> properties;
+
+ @JsonProperty("audit")
+ private AuditDTO audit;
+
+ @Override
+ public Audit auditInfo() {
+ return audit;
+ }
+
+ @Override
+ public int version() {
+ return version;
+ }
+
+ @Override
+ public String comment() {
+ return comment;
+ }
+
+ @Override
+ public String[] aliases() {
+ return aliases;
+ }
+
+ @Override
+ public String uri() {
+ return uri;
+ }
+
+ @Override
+ public Map<String, String> properties() {
+ return properties;
+ }
+
+ /**
+ * Creates a new builder for constructing a Model Version DTO.
+ *
+ * @return The builder.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Builder for constructing a Model Version DTO. */
+ public static class Builder {
+ private int version;
+ private String comment;
+ private String[] aliases;
+ private String uri;
+ private Map<String, String> properties;
+ private AuditDTO audit;
+
+ /**
+ * Sets the version number of the model version.
+ *
+ * @param version The version number.
+ * @return The builder.
+ */
+ public Builder withVersion(int version) {
+ this.version = version;
+ return this;
+ }
+
+ /**
+ * Sets the comment of the model version.
+ *
+ * @param comment The comment.
+ * @return The builder.
+ */
+ public Builder withComment(String comment) {
+ this.comment = comment;
+ return this;
+ }
+
+ /**
+ * Sets the aliases of the model version.
+ *
+ * @param aliases The aliases.
+ * @return The builder.
+ */
+ public Builder withAliases(String[] aliases) {
+ this.aliases = aliases;
+ return this;
+ }
+
+ /**
+ * Sets the URI of the model version.
+ *
+ * @param uri The URI.
+ * @return The builder.
+ */
+ public Builder withUri(String uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ /**
+ * Sets the properties of the model version.
+ *
+ * @param properties The properties.
+ * @return The builder.
+ */
+ public Builder withProperties(Map<String, String> properties) {
+ this.properties = properties;
+ return this;
+ }
+
+ /**
+ * Sets the audit information of the model version.
+ *
+ * @param audit The audit information.
+ * @return The builder.
+ */
+ public Builder withAudit(AuditDTO audit) {
+ this.audit = audit;
+ return this;
+ }
+
+ /**
+ * Builds the Model Version DTO.
+ *
+ * @return The Model Version DTO.
+ */
+ public ModelVersionDTO build() {
+ Preconditions.checkArgument(version >= 0, "Version must be
non-negative");
+ Preconditions.checkArgument(StringUtils.isNotBlank(uri), "URI cannot be
null or empty");
+ Preconditions.checkArgument(audit != null, "Audit cannot be null");
+
+ return new ModelVersionDTO(version, comment, aliases, uri, properties,
audit);
+ }
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/CatalogCreateRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/CatalogCreateRequest.java
index 3da6579676..d543ddb164 100644
---
a/common/src/main/java/org/apache/gravitino/dto/requests/CatalogCreateRequest.java
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/CatalogCreateRequest.java
@@ -18,8 +18,8 @@
*/
package org.apache.gravitino.dto.requests;
+import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonSetter;
import com.google.common.base.Preconditions;
import java.util.Map;
import javax.annotation.Nullable;
@@ -54,11 +54,6 @@ public class CatalogCreateRequest implements RESTRequest {
@JsonProperty("properties")
private final Map<String, String> properties;
- /** Default constructor for CatalogCreateRequest. */
- public CatalogCreateRequest() {
- this(null, null, null, null, null);
- }
-
/**
* Constructor for CatalogCreateRequest.
*
@@ -68,34 +63,24 @@ public class CatalogCreateRequest implements RESTRequest {
* @param comment The comment for the catalog.
* @param properties The properties for the catalog.
*/
+ @JsonCreator
public CatalogCreateRequest(
- String name,
- Catalog.Type type,
- String provider,
- String comment,
- Map<String, String> properties) {
+ @JsonProperty("name") String name,
+ @JsonProperty("type") Catalog.Type type,
+ @JsonProperty("provider") String provider,
+ @JsonProperty("comment") String comment,
+ @JsonProperty("properties") Map<String, String> properties) {
this.name = name;
this.type = type;
- this.provider = provider;
this.comment = comment;
this.properties = properties;
- }
- /**
- * Sets the provider of the catalog if it is null. The value of provider in
the request can be
- * null if the catalog is a managed catalog. For such request, the value
will be set when it is
- * deserialized.
- *
- * @param provider The provider of the catalog.
- */
- @JsonSetter(value = "provider")
- public void setProvider(String provider) {
if (StringUtils.isNotBlank(provider)) {
this.provider = provider;
} else if (type != null && type.supportsManagedCatalog()) {
this.provider = CatalogProvider.shortNameForManagedCatalog(type);
} else {
- throw new IllegalStateException(
+ throw new IllegalArgumentException(
"Provider cannot be null for catalog type "
+ type
+ " that doesn't support managed catalog");
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/ModelRegisterRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelRegisterRequest.java
new file mode 100644
index 0000000000..b9cd191617
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelRegisterRequest.java
@@ -0,0 +1,54 @@
+/*
+ * 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 lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to register a model. */
+@Getter
+@ToString
+@EqualsAndHashCode
+@NoArgsConstructor
+@AllArgsConstructor
+public class ModelRegisterRequest implements RESTRequest {
+
+ @JsonProperty("name")
+ private String name;
+
+ @JsonProperty("comment")
+ private String comment;
+
+ @JsonProperty("properties")
+ private Map<String, String> properties;
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(name), "\"name\" field is required and cannot
be empty");
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/ModelVersionLinkRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelVersionLinkRequest.java
new file mode 100644
index 0000000000..24e5932932
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelVersionLinkRequest.java
@@ -0,0 +1,67 @@
+/*
+ * 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 lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to link a model version. */
+@Getter
+@ToString
+@EqualsAndHashCode
+@NoArgsConstructor
+@AllArgsConstructor
+public class ModelVersionLinkRequest implements RESTRequest {
+
+ @JsonProperty("uri")
+ private String uri;
+
+ @JsonProperty("aliases")
+ private String[] aliases;
+
+ @JsonProperty("comment")
+ private String comment;
+
+ @JsonProperty("properties")
+ private Map<String, String> properties;
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(uri), "\"uri\" field is required and cannot be
empty");
+
+ if (aliases != null && aliases.length > 0) {
+ for (String alias : aliases) {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(alias), "alias must not be null or empty");
+ Preconditions.checkArgument(
+ !NumberUtils.isCreatable(alias), "alias must not be a number or a
number string");
+ }
+ }
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/ModelResponse.java
b/common/src/main/java/org/apache/gravitino/dto/responses/ModelResponse.java
new file mode 100644
index 0000000000..ac51cdd647
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/responses/ModelResponse.java
@@ -0,0 +1,59 @@
+/*
+ * 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.model.ModelDTO;
+
+/** Response for model response. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class ModelResponse extends BaseResponse {
+
+ @JsonProperty("model")
+ private final ModelDTO model;
+
+ /**
+ * Constructor for ModelResponse.
+ *
+ * @param model The model DTO object.
+ */
+ public ModelResponse(ModelDTO model) {
+ super(0);
+ this.model = model;
+ }
+
+ /** Default constructor for ModelResponse. (Used for Jackson
deserialization.) */
+ public ModelResponse() {
+ super();
+ this.model = null;
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ super.validate();
+
+ Preconditions.checkArgument(model != null, "model must not be null");
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionListResponse.java
b/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionListResponse.java
new file mode 100644
index 0000000000..4d3551e139
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionListResponse.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.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+/** Represents a response for a list of model versions. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class ModelVersionListResponse extends BaseResponse {
+
+ @JsonProperty("versions")
+ private int[] versions;
+
+ /**
+ * Constructor for ModelVersionListResponse.
+ *
+ * @param versions The list of model versions.
+ */
+ public ModelVersionListResponse(int[] versions) {
+ super(0);
+ this.versions = versions;
+ }
+
+ /** Default constructor for ModelVersionListResponse. (Used for Jackson
deserialization.) */
+ public ModelVersionListResponse() {
+ super();
+ this.versions = null;
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ super.validate();
+ Preconditions.checkArgument(versions != null, "versions cannot be null");
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionResponse.java
b/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionResponse.java
new file mode 100644
index 0000000000..8b21472833
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/responses/ModelVersionResponse.java
@@ -0,0 +1,59 @@
+/*
+ * 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.model.ModelVersionDTO;
+
+/** Represents a response for a model version. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class ModelVersionResponse extends BaseResponse {
+
+ @JsonProperty("modelVersion")
+ private final ModelVersionDTO modelVersion;
+
+ /**
+ * Constructor for ModelVersionResponse.
+ *
+ * @param modelVersion The model version DTO object.
+ */
+ public ModelVersionResponse(ModelVersionDTO modelVersion) {
+ super(0);
+ this.modelVersion = modelVersion;
+ }
+
+ /** Default constructor for ModelVersionResponse. (Used for Jackson
deserialization.) */
+ public ModelVersionResponse() {
+ super();
+ this.modelVersion = null;
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ super.validate();
+
+ Preconditions.checkArgument(modelVersion != null, "modelVersion must not
be null");
+ }
+}
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 254de8c324..ce63398e60 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
@@ -51,6 +51,8 @@ import org.apache.gravitino.dto.authorization.UserDTO;
import org.apache.gravitino.dto.credential.CredentialDTO;
import org.apache.gravitino.dto.file.FilesetDTO;
import org.apache.gravitino.dto.messaging.TopicDTO;
+import org.apache.gravitino.dto.model.ModelDTO;
+import org.apache.gravitino.dto.model.ModelVersionDTO;
import org.apache.gravitino.dto.rel.ColumnDTO;
import org.apache.gravitino.dto.rel.DistributionDTO;
import org.apache.gravitino.dto.rel.SortOrderDTO;
@@ -80,6 +82,8 @@ 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.model.Model;
+import org.apache.gravitino.model.ModelVersion;
import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.Table;
import org.apache.gravitino.rel.expressions.Expression;
@@ -629,6 +633,39 @@ public class DTOConverters {
.build();
}
+ /**
+ * Converts a Model to a ModelDTO.
+ *
+ * @param model The model to be converted.
+ * @return The model DTO.
+ */
+ public static ModelDTO toDTO(Model model) {
+ return ModelDTO.builder()
+ .withName(model.name())
+ .withComment(model.comment())
+ .withProperties(model.properties())
+ .withLatestVersion(model.latestVersion())
+ .withAudit(toDTO(model.auditInfo()))
+ .build();
+ }
+
+ /**
+ * Converts a ModelVersion to a ModelVersionDTO.
+ *
+ * @param modelVersion The model version to be converted.
+ * @return The model version DTO.
+ */
+ public static ModelVersionDTO toDTO(ModelVersion modelVersion) {
+ return ModelVersionDTO.builder()
+ .withVersion(modelVersion.version())
+ .withComment(modelVersion.comment())
+ .withAliases(modelVersion.aliases())
+ .withUri(modelVersion.uri())
+ .withProperties(modelVersion.properties())
+ .withAudit(toDTO(modelVersion.auditInfo()))
+ .build();
+ }
+
/**
* Converts an array of Columns to an array of ColumnDTOs.
*
diff --git
a/common/src/test/java/org/apache/gravitino/dto/model/TestModelDTO.java
b/common/src/test/java/org/apache/gravitino/dto/model/TestModelDTO.java
new file mode 100644
index 0000000000..39e4628eca
--- /dev/null
+++ b/common/src/test/java/org/apache/gravitino/dto/model/TestModelDTO.java
@@ -0,0 +1,81 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
+import java.time.Instant;
+import java.util.Map;
+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 TestModelDTO {
+
+ @Test
+ public void testModelSerDe() throws JsonProcessingException {
+ AuditDTO audit =
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+ Map<String, String> props = ImmutableMap.of("key", "value");
+
+ ModelDTO modelDTO =
+ ModelDTO.builder()
+ .withName("model_test")
+ .withComment("model comment")
+ .withLatestVersion(0)
+ .withProperties(props)
+ .withAudit(audit)
+ .build();
+
+ String serJson = JsonUtils.objectMapper().writeValueAsString(modelDTO);
+ ModelDTO deserModelDTO = JsonUtils.objectMapper().readValue(serJson,
ModelDTO.class);
+ Assertions.assertEquals(modelDTO, deserModelDTO);
+
+ // Test with null comment and properties
+ ModelDTO modelDTO1 =
+
ModelDTO.builder().withName("model_test").withLatestVersion(0).withAudit(audit).build();
+
+ String serJson1 = JsonUtils.objectMapper().writeValueAsString(modelDTO1);
+ ModelDTO deserModelDTO1 = JsonUtils.objectMapper().readValue(serJson1,
ModelDTO.class);
+ Assertions.assertEquals(modelDTO1, deserModelDTO1);
+ Assertions.assertNull(deserModelDTO1.comment());
+ Assertions.assertNull(deserModelDTO1.properties());
+ }
+
+ @Test
+ public void testInvalidModelDTO() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ ModelDTO.builder().build();
+ });
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+
ModelDTO.builder().withName("model_test").withLatestVersion(-1).build();
+ });
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+
ModelDTO.builder().withName("model_test").withLatestVersion(0).build();
+ });
+ }
+}
diff --git
a/common/src/test/java/org/apache/gravitino/dto/model/TestModelVersionDTO.java
b/common/src/test/java/org/apache/gravitino/dto/model/TestModelVersionDTO.java
new file mode 100644
index 0000000000..5251246c37
--- /dev/null
+++
b/common/src/test/java/org/apache/gravitino/dto/model/TestModelVersionDTO.java
@@ -0,0 +1,133 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableMap;
+import java.time.Instant;
+import java.util.Map;
+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 TestModelVersionDTO {
+
+ @Test
+ public void testModelVersionSerDe() throws JsonProcessingException {
+ AuditDTO audit =
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+ Map<String, String> props = ImmutableMap.of("key", "value");
+
+ ModelVersionDTO modelVersionDTO =
+ ModelVersionDTO.builder()
+ .withVersion(0)
+ .withComment("model version comment")
+ .withAliases(new String[] {"alias1", "alias2"})
+ .withUri("uri")
+ .withProperties(props)
+ .withAudit(audit)
+ .build();
+
+ String serJson =
JsonUtils.objectMapper().writeValueAsString(modelVersionDTO);
+ ModelVersionDTO deserModelVersionDTO =
+ JsonUtils.objectMapper().readValue(serJson, ModelVersionDTO.class);
+
+ Assertions.assertEquals(modelVersionDTO, deserModelVersionDTO);
+
+ // Test with null aliases
+ ModelVersionDTO modelVersionDTO1 =
+ ModelVersionDTO.builder()
+ .withVersion(0)
+ .withComment("model version comment")
+ .withUri("uri")
+ .withProperties(props)
+ .withAudit(audit)
+ .build();
+
+ String serJson1 =
JsonUtils.objectMapper().writeValueAsString(modelVersionDTO1);
+ ModelVersionDTO deserModelVersionDTO1 =
+ JsonUtils.objectMapper().readValue(serJson1, ModelVersionDTO.class);
+
+ Assertions.assertEquals(modelVersionDTO1, deserModelVersionDTO1);
+ Assertions.assertNull(deserModelVersionDTO1.aliases());
+
+ // Test with empty aliases
+ ModelVersionDTO modelVersionDTO2 =
+ ModelVersionDTO.builder()
+ .withVersion(0)
+ .withComment("model version comment")
+ .withAliases(new String[] {})
+ .withUri("uri")
+ .withProperties(props)
+ .withAudit(audit)
+ .build();
+
+ String serJson2 =
JsonUtils.objectMapper().writeValueAsString(modelVersionDTO2);
+ ModelVersionDTO deserModelVersionDTO2 =
+ JsonUtils.objectMapper().readValue(serJson2, ModelVersionDTO.class);
+
+ Assertions.assertEquals(modelVersionDTO2, deserModelVersionDTO2);
+ Assertions.assertArrayEquals(new String[] {},
deserModelVersionDTO2.aliases());
+
+ // Test with null comment and properties
+ ModelVersionDTO modelVersionDTO3 =
+
ModelVersionDTO.builder().withVersion(0).withUri("uri").withAudit(audit).build();
+
+ String serJson3 =
JsonUtils.objectMapper().writeValueAsString(modelVersionDTO3);
+ ModelVersionDTO deserModelVersionDTO3 =
+ JsonUtils.objectMapper().readValue(serJson3, ModelVersionDTO.class);
+
+ Assertions.assertEquals(modelVersionDTO3, deserModelVersionDTO3);
+ Assertions.assertNull(deserModelVersionDTO3.comment());
+ Assertions.assertNull(deserModelVersionDTO3.properties());
+ }
+
+ @Test
+ public void testInvalidModelVersionDTO() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ ModelVersionDTO.builder().build();
+ });
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ ModelVersionDTO.builder().withVersion(-1).build();
+ });
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ ModelVersionDTO.builder().withVersion(0).build();
+ });
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ ModelVersionDTO.builder().withVersion(0).withUri("").build();
+ });
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ ModelVersionDTO.builder().withVersion(0).withUri("uri").build();
+ });
+ }
+}
diff --git
a/common/src/test/java/org/apache/gravitino/dto/requests/TestCatalogCreateRequest.java
b/common/src/test/java/org/apache/gravitino/dto/requests/TestCatalogCreateRequest.java
index b4b7383a7e..3b5221ff1a 100644
---
a/common/src/test/java/org/apache/gravitino/dto/requests/TestCatalogCreateRequest.java
+++
b/common/src/test/java/org/apache/gravitino/dto/requests/TestCatalogCreateRequest.java
@@ -57,19 +57,24 @@ public class TestCatalogCreateRequest {
String serJson1 = JsonUtils.objectMapper().writeValueAsString(request1);
CatalogCreateRequest deserRequest1 =
JsonUtils.objectMapper().readValue(serJson1,
CatalogCreateRequest.class);
-
Assertions.assertEquals(
deserRequest1.getType().name().toLowerCase(Locale.ROOT),
deserRequest1.getProvider());
Assertions.assertNull(deserRequest1.getComment());
Assertions.assertNull(deserRequest1.getProperties());
+ String json = "{\"name\":\"catalog_test\",\"type\":\"model\"}";
+ CatalogCreateRequest deserRequest2 =
+ JsonUtils.objectMapper().readValue(json, CatalogCreateRequest.class);
+ Assertions.assertEquals("model", deserRequest2.getProvider());
+
// Test using null provider with catalog type doesn't support managed
catalog
- CatalogCreateRequest request2 =
- new CatalogCreateRequest("catalog_test", Catalog.Type.RELATIONAL,
null, null, null);
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> new CatalogCreateRequest("catalog_test",
Catalog.Type.RELATIONAL, null, null, null));
- String serJson2 = JsonUtils.objectMapper().writeValueAsString(request2);
+ String json1 = "{\"name\":\"catalog_test\",\"type\":\"relational\"}";
Assertions.assertThrows(
JsonMappingException.class,
- () -> JsonUtils.objectMapper().readValue(serJson2,
CatalogCreateRequest.class));
+ () -> JsonUtils.objectMapper().readValue(json1,
CatalogCreateRequest.class));
}
}
diff --git
a/common/src/test/java/org/apache/gravitino/dto/requests/TestModelRegisterRequest.java
b/common/src/test/java/org/apache/gravitino/dto/requests/TestModelRegisterRequest.java
new file mode 100644
index 0000000000..09dbdb8c31
--- /dev/null
+++
b/common/src/test/java/org/apache/gravitino/dto/requests/TestModelRegisterRequest.java
@@ -0,0 +1,47 @@
+/*
+ * 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 TestModelRegisterRequest {
+
+ @Test
+ public void testModelRegisterRequestSerDe() throws JsonProcessingException {
+ Map<String, String> props = ImmutableMap.of("key", "value");
+ ModelRegisterRequest req = new ModelRegisterRequest("model", "comment",
props);
+
+ String serJson = JsonUtils.objectMapper().writeValueAsString(req);
+ ModelRegisterRequest deserReq =
+ JsonUtils.objectMapper().readValue(serJson,
ModelRegisterRequest.class);
+ Assertions.assertEquals(req, deserReq);
+
+ // Test with null comment and properties
+ ModelRegisterRequest req1 = new ModelRegisterRequest("model", null, null);
+ String serJson1 = JsonUtils.objectMapper().writeValueAsString(req1);
+ ModelRegisterRequest deserReq1 =
+ JsonUtils.objectMapper().readValue(serJson1,
ModelRegisterRequest.class);
+ Assertions.assertEquals(req1, deserReq1);
+ }
+}
diff --git
a/common/src/test/java/org/apache/gravitino/dto/requests/TestModelVersionLinkRequest.java
b/common/src/test/java/org/apache/gravitino/dto/requests/TestModelVersionLinkRequest.java
new file mode 100644
index 0000000000..4c0df6d73e
--- /dev/null
+++
b/common/src/test/java/org/apache/gravitino/dto/requests/TestModelVersionLinkRequest.java
@@ -0,0 +1,75 @@
+/*
+ * 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 TestModelVersionLinkRequest {
+
+ @Test
+ public void testModelVersionLinkRequestSerDe() throws
JsonProcessingException {
+ Map<String, String> props = ImmutableMap.of("key", "value");
+ ModelVersionLinkRequest request =
+ new ModelVersionLinkRequest("uri", new String[] {"alias1", "alias2"},
"comment", props);
+
+ String serJson = JsonUtils.objectMapper().writeValueAsString(request);
+ ModelVersionLinkRequest deserRequest =
+ JsonUtils.objectMapper().readValue(serJson,
ModelVersionLinkRequest.class);
+
+ Assertions.assertEquals(request, deserRequest);
+
+ // Test with null aliases
+ ModelVersionLinkRequest request1 = new ModelVersionLinkRequest("uri",
null, "comment", props);
+
+ String serJson1 = JsonUtils.objectMapper().writeValueAsString(request1);
+ ModelVersionLinkRequest deserRequest1 =
+ JsonUtils.objectMapper().readValue(serJson1,
ModelVersionLinkRequest.class);
+
+ Assertions.assertEquals(request1, deserRequest1);
+ Assertions.assertNull(deserRequest1.getAliases());
+
+ // Test with empty aliases
+ ModelVersionLinkRequest request2 =
+ new ModelVersionLinkRequest("uri", new String[] {}, "comment", props);
+
+ String serJson2 = JsonUtils.objectMapper().writeValueAsString(request2);
+ ModelVersionLinkRequest deserRequest2 =
+ JsonUtils.objectMapper().readValue(serJson2,
ModelVersionLinkRequest.class);
+
+ Assertions.assertEquals(request2, deserRequest2);
+ Assertions.assertEquals(0, deserRequest2.getAliases().length);
+
+ // Test with null comment and properties
+ ModelVersionLinkRequest request3 =
+ new ModelVersionLinkRequest("uri", new String[] {"alias1", "alias2"},
null, null);
+
+ String serJson3 = JsonUtils.objectMapper().writeValueAsString(request3);
+ ModelVersionLinkRequest deserRequest3 =
+ JsonUtils.objectMapper().readValue(serJson3,
ModelVersionLinkRequest.class);
+
+ Assertions.assertEquals(request3, deserRequest3);
+ Assertions.assertNull(deserRequest3.getComment());
+ Assertions.assertNull(deserRequest3.getProperties());
+ }
+}
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 5f94782022..57813c0bc6 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
@@ -26,8 +26,10 @@ 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.ImmutableMap;
import com.google.common.collect.Lists;
import java.time.Instant;
+import java.util.Map;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.authorization.Privileges;
@@ -41,6 +43,8 @@ import org.apache.gravitino.dto.authorization.GroupDTO;
import org.apache.gravitino.dto.authorization.RoleDTO;
import org.apache.gravitino.dto.authorization.SecurableObjectDTO;
import org.apache.gravitino.dto.authorization.UserDTO;
+import org.apache.gravitino.dto.model.ModelDTO;
+import org.apache.gravitino.dto.model.ModelVersionDTO;
import org.apache.gravitino.dto.rel.ColumnDTO;
import org.apache.gravitino.dto.rel.TableDTO;
import org.apache.gravitino.dto.rel.partitioning.Partitioning;
@@ -390,4 +394,76 @@ public class TestResponses {
FileLocationResponse response = new FileLocationResponse();
assertThrows(IllegalArgumentException.class, () -> response.validate());
}
+
+ @Test
+ void testModelResponse() throws JsonProcessingException {
+ Map<String, String> props = ImmutableMap.of("key", "value");
+ AuditDTO audit =
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+
+ ModelDTO modelDTO =
+ ModelDTO.builder()
+ .withName("model1")
+ .withLatestVersion(0)
+ .withComment("comment1")
+ .withProperties(props)
+ .withAudit(audit)
+ .build();
+
+ ModelResponse response = new ModelResponse(modelDTO);
+ String serJson = JsonUtils.objectMapper().writeValueAsString(response);
+ ModelResponse deserResponse = JsonUtils.objectMapper().readValue(serJson,
ModelResponse.class);
+
+ assertEquals(response, deserResponse);
+
+ ModelResponse response1 = new ModelResponse();
+ assertThrows(IllegalArgumentException.class, response1::validate);
+ }
+
+ @Test
+ void testModelVersionListResponse() throws JsonProcessingException {
+ ModelVersionListResponse response1 = new ModelVersionListResponse(new
int[] {});
+ assertDoesNotThrow(response1::validate);
+
+ String serJson1 = JsonUtils.objectMapper().writeValueAsString(response1);
+ ModelVersionListResponse deserResponse1 =
+ JsonUtils.objectMapper().readValue(serJson1,
ModelVersionListResponse.class);
+ assertEquals(response1, deserResponse1);
+ assertArrayEquals(new int[] {}, deserResponse1.getVersions());
+
+ ModelVersionListResponse response2 = new ModelVersionListResponse(new
int[] {1, 2});
+ assertDoesNotThrow(response2::validate);
+
+ String serJson2 = JsonUtils.objectMapper().writeValueAsString(response2);
+ ModelVersionListResponse deserResponse2 =
+ JsonUtils.objectMapper().readValue(serJson2,
ModelVersionListResponse.class);
+ assertEquals(response2, deserResponse2);
+ assertArrayEquals(new int[] {1, 2}, deserResponse2.getVersions());
+ }
+
+ @Test
+ void testModelVersionResponse() throws JsonProcessingException {
+ Map<String, String> props = ImmutableMap.of("key", "value");
+ AuditDTO audit =
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+
+ ModelVersionDTO modelVersionDTO =
+ ModelVersionDTO.builder()
+ .withVersion(0)
+ .withComment("model version comment")
+ .withAliases(new String[] {"alias1", "alias2"})
+ .withUri("uri")
+ .withProperties(props)
+ .withAudit(audit)
+ .build();
+
+ ModelVersionResponse response = new ModelVersionResponse(modelVersionDTO);
+ response.validate(); // No exception thrown
+
+ String serJson = JsonUtils.objectMapper().writeValueAsString(response);
+ ModelVersionResponse deserResponse =
+ JsonUtils.objectMapper().readValue(serJson,
ModelVersionResponse.class);
+ assertEquals(response, deserResponse);
+
+ ModelVersionResponse response1 = new ModelVersionResponse();
+ assertThrows(IllegalArgumentException.class, response1::validate);
+ }
}
diff --git a/docs/open-api/models.yaml b/docs/open-api/models.yaml
new file mode 100644
index 0000000000..713a7037cd
--- /dev/null
+++ b/docs/open-api/models.yaml
@@ -0,0 +1,561 @@
+# 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.
+
+---
+
+paths:
+
+ /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models:
+ parameters:
+ - $ref: "./openapi.yaml#/components/parameters/metalake"
+ - $ref: "./openapi.yaml#/components/parameters/catalog"
+ - $ref: "./openapi.yaml#/components/parameters/schema"
+
+ get:
+ tags:
+ - model
+ summary: List models
+ operationId: listModels
+ responses:
+ "200":
+ $ref: "./openapi.yaml#/components/responses/EntityListResponse"
+ "400":
+ $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+ post:
+ tags:
+ - model
+ summary: Register model
+ operationId: registerModel
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ModelRegisterRequest"
+ examples:
+ ModelRegisterRequest:
+ $ref: "#/components/examples/ModelRegisterRequest"
+ responses:
+ "200":
+ $ref: "#/components/responses/ModelResponse"
+ "409":
+ description: Conflict - The target model already exists
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ ModelAlreadyExistsErrorResponse:
+ $ref: "#/components/examples/ModelAlreadyExistsException"
+ "404":
+ description: Not Found - The schema does not exist
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ NoSuchSchemaException:
+ $ref:
"./schemas.yaml#/components/examples/NoSuchSchemaException"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+ /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}:
+ parameters:
+ - $ref: "./openapi.yaml#/components/parameters/metalake"
+ - $ref: "./openapi.yaml#/components/parameters/catalog"
+ - $ref: "./openapi.yaml#/components/parameters/schema"
+ - $ref: "./openapi.yaml#/components/parameters/model"
+
+ get:
+ tags:
+ - model
+ summary: Get model
+ operationId: getModel
+ description: Returns the specified model object
+ responses:
+ "200":
+ $ref: "#/components/responses/ModelResponse"
+ "404":
+ description: Not Found - The target fileset does not exist
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ NoSuchMetalakeException:
+ $ref:
"./metalakes.yaml#/components/examples/NoSuchMetalakeException"
+ NoSuchCatalogException:
+ $ref:
"./catalogs.yaml#/components/examples/NoSuchCatalogException"
+ NoSuchSchemaException:
+ $ref:
"./schemas.yaml#/components/examples/NoSuchSchemaException"
+ NoSuchModelException:
+ $ref: "#/components/examples/NoSuchModelException"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+ delete:
+ tags:
+ - model
+ summary: delete model
+ operationId: deleteModel
+ responses:
+ "200":
+ $ref: "./openapi.yaml#/components/responses/DropResponse"
+ "400":
+ $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+ post:
+ tags:
+ - model
+ summary: link model version
+ operationId: linkModelVersion
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ModelVersionLinkRequest"
+ examples:
+ ModelVersionLinkRequest:
+ $ref: "#/components/examples/ModelVersionLinkRequest"
+ responses:
+ "200":
+ $ref: "./openapi.yaml#/components/responses/BaseResponse"
+ "404":
+ description: Not Found - The target model does not exist
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ NoSuchModelException:
+ $ref: "#/components/examples/NoSuchModelException"
+ "409":
+ description: Conflict - The model version aliases already exist
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ ModelVersionAliasesAlreadyExistException:
+ $ref:
"#/components/examples/ModelVersionAliasesAlreadyExistException"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/versions:
+ parameters:
+ - $ref: "./openapi.yaml#/components/parameters/metalake"
+ - $ref: "./openapi.yaml#/components/parameters/catalog"
+ - $ref: "./openapi.yaml#/components/parameters/schema"
+ - $ref: "./openapi.yaml#/components/parameters/model"
+
+ get:
+ tags:
+ - model
+ summary: List model versions
+ operationId: listModelVersions
+ responses:
+ "200":
+ $ref: "#/components/responses/ModelVersionListResponse"
+ "404":
+ description: Not Found - The target model does not exist
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ NoSuchModelException:
+ $ref: "#/components/examples/NoSuchModelException"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/versions/{version}:
+ parameters:
+ - $ref: "./openapi.yaml#/components/parameters/metalake"
+ - $ref: "./openapi.yaml#/components/parameters/catalog"
+ - $ref: "./openapi.yaml#/components/parameters/schema"
+ - $ref: "./openapi.yaml#/components/parameters/model"
+ - $ref: "#/components/parameters/version"
+
+ get:
+ tags:
+ - model
+ summary: Get model version
+ operationId: getModelVersion
+ responses:
+ "200":
+ $ref: "#/components/responses/ModelVersionResponse"
+ "404":
+ description: Not Found - The target model version does not exist
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ NoSuchModelVersionException:
+ $ref: "#/components/examples/NoSuchModelVersionException"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+ delete:
+ tags:
+ - model
+ summary: delete model version
+ operationId: deleteModelVersion
+ responses:
+ "200":
+ $ref: "./openapi.yaml#/components/responses/DropResponse"
+ "400":
+ $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/aliases/{alias}:
+ parameters:
+ - $ref: "./openapi.yaml#/components/parameters/metalake"
+ - $ref: "./openapi.yaml#/components/parameters/catalog"
+ - $ref: "./openapi.yaml#/components/parameters/schema"
+ - $ref: "./openapi.yaml#/components/parameters/model"
+ - $ref: "#/components/parameters/alias"
+
+ get:
+ tags:
+ - model
+ summary: Get model version by alias
+ operationId: getModelVersionByAlias
+ responses:
+ "200":
+ $ref: "#/components/responses/ModelVersionResponse"
+ "404":
+ description: Not Found - The target model version does not exist
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ NoSuchModelVersionException:
+ $ref: "#/components/examples/NoSuchModelVersionException"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+ delete:
+ tags:
+ - model
+ summary: delete model version by alias
+ operationId: deleteModelVersionByAlias
+ responses:
+ "200":
+ $ref: "./openapi.yaml#/components/responses/DropResponse"
+ "400":
+ $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+components:
+ parameters:
+ version:
+ name: version
+ in: path
+ required: true
+ description: The version of the model
+ schema:
+ type: integer
+ alias:
+ name: alias
+ in: path
+ required: true
+ description: The alias of the model version
+ schema:
+ type: string
+
+ schemas:
+ Model:
+ type: object
+ required:
+ - name
+ - audit
+ - latestVersion
+ properties:
+ name:
+ type: string
+ description: The name of the model
+ latestVersion:
+ type: integer
+ description: The latest version of the model
+ comment:
+ type: string
+ description: The comment of the fileset
+ nullable: true
+ properties:
+ type: object
+ description: The properties of the fileset
+ nullable: true
+ default: {}
+ additionalProperties:
+ type: string
+ audit:
+ $ref: "./openapi.yaml#/components/schemas/Audit"
+
+ ModelVersion:
+ type: object
+ required:
+ - uri
+ - version
+ - audit
+ properties:
+ uri:
+ type: string
+ description: The uri of the model version
+ version:
+ type: integer
+ description: The version of the model
+ aliases:
+ type: array
+ description: The aliases of the model version
+ nullable: true
+ items:
+ type: string
+ comment:
+ type: string
+ description: The comment of the model version
+ nullable: true
+ properties:
+ type: object
+ description: The properties of the model version
+ nullable: true
+ default: {}
+ additionalProperties:
+ type: string
+ audit:
+ $ref: "./openapi.yaml#/components/schemas/Audit"
+
+ ModelRegisterRequest:
+ type: object
+ required:
+ - name
+ properties:
+ name:
+ type: string
+ description: The name of the model. Can not be empty.
+ comment:
+ type: string
+ description: The comment of the model. Can be empty.
+ nullable: true
+ properties:
+ type: object
+ description: The properties of the model. Can be empty.
+ nullable: true
+ default: {}
+ additionalProperties:
+ type: string
+
+ ModelVersionLinkRequest:
+ type: object
+ required:
+ - uri
+ properties:
+ uri:
+ type: string
+ description: The uri of the model version
+ aliases:
+ type: array
+ description: The aliases of the model version
+ nullable: true
+ items:
+ type: string
+ comment:
+ type: string
+ description: The comment of the model version
+ nullable: true
+ properties:
+ type: object
+ description: The properties of the model version
+ nullable: true
+ default: {}
+ additionalProperties:
+ type: string
+
+ responses:
+ ModelResponse:
+ description: The response of model object
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ type: object
+ properties:
+ code:
+ type: integer
+ format: int32
+ description: Status code of the response
+ enum:
+ - 0
+ model:
+ $ref: "#/components/schemas/Model"
+ examples:
+ ModelResponse:
+ $ref: "#/components/examples/ModelResponse"
+ ModelVersionListResponse:
+ description: The response of model version list
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ type: object
+ properties:
+ code:
+ type: integer
+ format: int32
+ description: Status code of the response
+ enum:
+ - 0
+ versions:
+ type: array
+ description: The list of model versions
+ items:
+ format: int32
+ examples:
+ ModelVersionListResponse:
+ $ref: "#/components/examples/ModelVersionListResponse"
+ ModelVersionResponse:
+ description: The response of model version object
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ type: object
+ properties:
+ code:
+ type: integer
+ format: int32
+ description: Status code of the response
+ enum:
+ - 0
+ modelVersion:
+ $ref: "#/components/schemas/ModelVersion"
+ examples:
+ ModelResponse:
+ $ref: "#/components/examples/ModelVersionResponse"
+
+ examples:
+ ModelRegisterRequest:
+ value: {
+ "name": "model1",
+ "comment": "This is a comment",
+ "properties": {
+ "key1": "value1",
+ "key2": "value2"
+ }
+ }
+
+ ModelVersionLinkRequest:
+ value: {
+ "uri": "hdfs://path/to/model",
+ "aliases": ["alias1", "alias2"],
+ "comment": "This is a comment",
+ "properties": {
+ "key1": "value1",
+ "key2": "value2"
+ }
+ }
+
+ ModelResponse:
+ value: {
+ "code": 0,
+ "model" : {
+ "name": "model1",
+ "latestVersion": 0,
+ "comment": "This is a comment",
+ "properties": {
+ "key1": "value1",
+ "key2": "value2"
+ },
+ "audit": {
+ "creator": "user1",
+ "createTime": "2021-01-01T00:00:00Z",
+ "lastModifier": "user1",
+ "lastModifiedTime": "2021-01-01T00:00:00Z"
+ }
+ }
+ }
+
+ ModelVersionListResponse:
+ value: {
+ "code": 0,
+ "versions": [0, 1, 2]
+ }
+
+ ModelVersionResponse:
+ value: {
+ "code": 0,
+ "modelVersion" : {
+ "uri": "hdfs://path/to/model",
+ "version": 0,
+ "aliases": ["alias1", "alias2"],
+ "comment": "This is a comment",
+ "properties": {
+ "key1": "value1",
+ "key2": "value2"
+ },
+ "audit": {
+ "creator": "user1",
+ "createTime": "2021-01-01T00:00:00Z",
+ "lastModifier": "user1",
+ "lastModifiedTime": "2021-01-01T00:00:00Z"
+ }
+ }
+ }
+
+ ModelAlreadyExistsException:
+ value: {
+ "code": 1004,
+ "type": "ModelAlreadyExistsException",
+ "message": "Model already exists",
+ "stack": [
+ "org.apache.gravitino.exceptions.ModelAlreadyExistsException:
Model already exists"
+ ]
+ }
+
+ NoSuchModelException:
+ value: {
+ "code": 1003,
+ "type": "NoSuchModelException",
+ "message": "Model does not exist",
+ "stack": [
+ "org.apache.gravitino.exceptions.NoSuchModelException: Model does
not exist"
+ ]
+ }
+
+ ModelVersionAliasesAlreadyExistException:
+ value: {
+ "code": 1004,
+ "type": "ModelVersionAliasesAlreadyExistException",
+ "message": "Model version aliases already exist",
+ "stack": [
+
"org.apache.gravitino.exceptions.ModelVersionAliasesAlreadyExistException:
Model version aliases already exist"
+ ]
+ }
+
+ NoSuchModelVersionException:
+ value: {
+ "code": 1003,
+ "type": "NoSuchModelVersionException",
+ "message": "Model version does not exist",
+ "stack": [
+ "org.apache.gravitino.exceptions.NoSuchModelVersionException: Model
version does not exist"
+ ]
+ }
diff --git a/docs/open-api/openapi.yaml b/docs/open-api/openapi.yaml
index dd0564a7f9..d0c941ab47 100644
--- a/docs/open-api/openapi.yaml
+++ b/docs/open-api/openapi.yaml
@@ -113,6 +113,20 @@ paths:
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/topics/{topic}:
$ref:
"./topics.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1topics~1%7Btopic%7D"
+ /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models:
+ $ref:
"./models.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1models"
+
+ /metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}:
+ $ref:
"./models.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1models~1%7Bmodel%7D"
+
+
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/versions:
+ $ref:
"./models.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1models~1%7Bmodel%7D~1versions"
+
+
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/versions/{version}:
+ $ref:
"./models.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1models~1%7Bmodel%7D~1versions~1%7Bversion%7D"
+
+
/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models/{model}/aliases/{alias}:
+ $ref:
"./models.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1catalogs~1%7Bcatalog%7D~1schemas~1%7Bschema%7D~1models~1%7Bmodel%7D~1aliases~1%7Balias%7D"
/metalakes/{metalake}/users:
$ref: "./users.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1users"
@@ -430,6 +444,14 @@ components:
schema:
type: string
+ model:
+ name: model
+ in: path
+ description: The name of the model
+ required: true
+ schema:
+ type: string
+
tag:
name: tag
in: path
@@ -476,6 +498,7 @@ components:
- "COLUMN"
- "FILESET"
- "TOPIC"
+ - "MODEL"
- "ROLE"
metadataObjectFullName:
name: metadataObjectFullName
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 63e53aefd5..2afc65482b 100644
--- a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
+++ b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
@@ -27,6 +27,7 @@ import org.apache.gravitino.Configs;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.catalog.CatalogDispatcher;
import org.apache.gravitino.catalog.FilesetDispatcher;
+import org.apache.gravitino.catalog.ModelDispatcher;
import org.apache.gravitino.catalog.PartitionDispatcher;
import org.apache.gravitino.catalog.SchemaDispatcher;
import org.apache.gravitino.catalog.TableDispatcher;
@@ -118,6 +119,7 @@ public class GravitinoServer extends ResourceConfig {
bind(gravitinoEnv.credentialOperationDispatcher())
.to(CredentialOperationDispatcher.class)
.ranked(1);
+
bind(gravitinoEnv.modelDispatcher()).to(ModelDispatcher.class).ranked(1);
}
});
register(JsonProcessingExceptionMapper.class);
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 faf94f5064..b71219b045 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
@@ -32,6 +32,8 @@ import org.apache.gravitino.exceptions.InUseException;
import org.apache.gravitino.exceptions.MetalakeAlreadyExistsException;
import org.apache.gravitino.exceptions.MetalakeInUseException;
import org.apache.gravitino.exceptions.MetalakeNotInUseException;
+import org.apache.gravitino.exceptions.ModelAlreadyExistsException;
+import
org.apache.gravitino.exceptions.ModelVersionAliasesAlreadyExistException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
import org.apache.gravitino.exceptions.NonEmptyCatalogException;
import org.apache.gravitino.exceptions.NonEmptyMetalakeException;
@@ -126,6 +128,11 @@ public class ExceptionHandlers {
return CredentialExceptionHandler.INSTANCE.handle(op, metadataObjectName,
"", e);
}
+ public static Response handleModelException(
+ OperationType op, String model, String schema, Exception e) {
+ return ModelExceptionHandler.INSTANCE.handle(op, model, schema, e);
+ }
+
public static Response handleTestConnectionException(Exception e) {
ErrorResponse response;
if (e instanceof IllegalArgumentException) {
@@ -729,6 +736,44 @@ public class ExceptionHandlers {
}
}
+ private static class ModelExceptionHandler extends BaseExceptionHandler {
+ private static final ExceptionHandler INSTANCE = new
ModelExceptionHandler();
+
+ private static String getModelErrorMsg(
+ String model, String operation, String schema, String reason) {
+ return String.format(
+ "Failed to operate model(s)%s operation [%s] under schema [%s],
reason [%s]",
+ model, operation, schema, reason);
+ }
+
+ @Override
+ public Response handle(OperationType op, String model, String schema,
Exception e) {
+ String formatted = StringUtil.isBlank(model) ? "" : " [" + model + "]";
+ String errorMsg = getModelErrorMsg(formatted, op.name(), schema,
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 ModelAlreadyExistsException
+ || e instanceof ModelVersionAliasesAlreadyExistException) {
+ return Utils.alreadyExists(errorMsg, e);
+
+ } else if (e instanceof ForbiddenException) {
+ return Utils.forbidden(errorMsg, e);
+
+ } else if (e instanceof NotInUseException) {
+ return Utils.notInUse(errorMsg, e);
+
+ } else {
+ return super.handle(op, model, schema, e);
+ }
+ }
+ }
+
@VisibleForTesting
static class BaseExceptionHandler extends ExceptionHandler {
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/ModelOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/ModelOperations.java
new file mode 100644
index 0000000000..fd50782108
--- /dev/null
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/ModelOperations.java
@@ -0,0 +1,411 @@
+/*
+ * 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 javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.catalog.ModelDispatcher;
+import org.apache.gravitino.dto.requests.ModelRegisterRequest;
+import org.apache.gravitino.dto.requests.ModelVersionLinkRequest;
+import org.apache.gravitino.dto.responses.BaseResponse;
+import org.apache.gravitino.dto.responses.DropResponse;
+import org.apache.gravitino.dto.responses.EntityListResponse;
+import org.apache.gravitino.dto.responses.ModelResponse;
+import org.apache.gravitino.dto.responses.ModelVersionListResponse;
+import org.apache.gravitino.dto.responses.ModelVersionResponse;
+import org.apache.gravitino.dto.util.DTOConverters;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.model.ModelVersion;
+import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.gravitino.utils.NamespaceUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Path("metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/models")
+public class ModelOperations {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(ModelOperations.class);
+
+ private final ModelDispatcher modelDispatcher;
+
+ @Context private HttpServletRequest httpRequest;
+
+ @Inject
+ public ModelOperations(ModelDispatcher modelDispatcher) {
+ this.modelDispatcher = modelDispatcher;
+ }
+
+ @GET
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "list-model." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
+ @ResponseMetered(name = "list-model", absolute = true)
+ public Response listModels(
+ @PathParam("metalake") String metalake,
+ @PathParam("catalog") String catalog,
+ @PathParam("schema") String schema) {
+ LOG.info("Received list models request for schema: {}.{}.{}", metalake,
catalog, schema);
+ Namespace modelNs = NamespaceUtil.ofModel(metalake, catalog, schema);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ NameIdentifier[] modelIds = modelDispatcher.listModels(modelNs);
+ modelIds = modelIds == null ? new NameIdentifier[0] : modelIds;
+ LOG.info("List {} models under schema {}", modelIds.length,
modelNs);
+ return Utils.ok(new EntityListResponse(modelIds));
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleModelException(OperationType.LIST, "",
schema, e);
+ }
+ }
+
+ @GET
+ @Path("{model}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "get-model." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
+ @ResponseMetered(name = "get-model", absolute = true)
+ public Response getModel(
+ @PathParam("metalake") String metalake,
+ @PathParam("catalog") String catalog,
+ @PathParam("schema") String schema,
+ @PathParam("model") String model) {
+ LOG.info("Received get model request: {}.{}.{}.{}", metalake, catalog,
schema, model);
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, model);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ Model m = modelDispatcher.getModel(modelId);
+ LOG.info("Model got: {}", modelId);
+ return Utils.ok(new ModelResponse(DTOConverters.toDTO(m)));
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleModelException(OperationType.GET, model,
schema, e);
+ }
+ }
+
+ @POST
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "register-model." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "register-model", absolute = true)
+ public Response registerModel(
+ @PathParam("metalake") String metalake,
+ @PathParam("catalog") String catalog,
+ @PathParam("schema") String schema,
+ ModelRegisterRequest request) {
+ LOG.info(
+ "Received register model request: {}.{}.{}.{}",
+ metalake,
+ catalog,
+ schema,
+ request.getName());
+
+ try {
+ request.validate();
+ NameIdentifier modelId =
+ NameIdentifierUtil.ofModel(metalake, catalog, schema,
request.getName());
+
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ Model m =
+ modelDispatcher.registerModel(
+ modelId, request.getComment(), request.getProperties());
+ LOG.info("Model registered: {}", modelId);
+ return Utils.ok(new ModelResponse(DTOConverters.toDTO(m)));
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleModelException(
+ OperationType.REGISTER, request.getName(), schema, e);
+ }
+ }
+
+ @DELETE
+ @Path("{model}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "delete-model." + MetricNames.HTTP_PROCESS_DURATION, absolute
= true)
+ @ResponseMetered(name = "delete-model", absolute = true)
+ public Response deleteModel(
+ @PathParam("metalake") String metalake,
+ @PathParam("catalog") String catalog,
+ @PathParam("schema") String schema,
+ @PathParam("model") String model) {
+ LOG.info("Received delete model request: {}.{}.{}.{}", metalake, catalog,
schema, model);
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, model);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ boolean deleted = modelDispatcher.deleteModel(modelId);
+ if (!deleted) {
+ LOG.warn("Cannot find to be deleted model {} under schema {}",
model, schema);
+ } else {
+ LOG.info("Model deleted: {}", modelId);
+ }
+
+ return Utils.ok(new DropResponse(deleted));
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleModelException(OperationType.DELETE,
model, schema, e);
+ }
+ }
+
+ @GET
+ @Path("{model}/versions")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "list-model-versions." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "list-model-versions", absolute = true)
+ public Response listModelVersions(
+ @PathParam("metalake") String metalake,
+ @PathParam("catalog") String catalog,
+ @PathParam("schema") String schema,
+ @PathParam("model") String model) {
+ LOG.info("Received list model versions request: {}.{}.{}.{}", metalake,
catalog, schema, model);
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, model);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ int[] versions = modelDispatcher.listModelVersions(modelId);
+ versions = versions == null ? new int[0] : versions;
+ LOG.info("List {} versions of model {}", versions.length, modelId);
+ return Utils.ok(new ModelVersionListResponse(versions));
+ });
+
+ } catch (Exception e) {
+ return
ExceptionHandlers.handleModelException(OperationType.LIST_VERSIONS, model,
schema, e);
+ }
+ }
+
+ @GET
+ @Path("{model}/versions/{version}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "get-model-version." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "get-model-version", absolute = true)
+ public Response getModelVersion(
+ @PathParam("metalake") String metalake,
+ @PathParam("catalog") String catalog,
+ @PathParam("schema") String schema,
+ @PathParam("model") String model,
+ @PathParam("version") int version) {
+ LOG.info(
+ "Received get model version request: {}.{}.{}.{}.{}",
+ metalake,
+ catalog,
+ schema,
+ model,
+ version);
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, model);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ ModelVersion mv = modelDispatcher.getModelVersion(modelId,
version);
+ LOG.info("Model version got: {}.{}", modelId, version);
+ return Utils.ok(new ModelVersionResponse(DTOConverters.toDTO(mv)));
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleModelException(
+ OperationType.GET, versionString(model, version), schema, e);
+ }
+ }
+
+ @GET
+ @Path("{model}/aliases/{alias}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "get-model-alias." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "get-model-alias", absolute = true)
+ public Response getModelVersionByAlias(
+ @PathParam("metalake") String metalake,
+ @PathParam("catalog") String catalog,
+ @PathParam("schema") String schema,
+ @PathParam("model") String model,
+ @PathParam("alias") String alias) {
+ LOG.info(
+ "Received get model version alias request: {}.{}.{}.{}.{}",
+ metalake,
+ catalog,
+ schema,
+ model,
+ alias);
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, model);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ ModelVersion mv = modelDispatcher.getModelVersion(modelId, alias);
+ LOG.info("Model version alias got: {}.{}", modelId, alias);
+ return Utils.ok(new ModelVersionResponse(DTOConverters.toDTO(mv)));
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleModelException(
+ OperationType.GET, aliasString(model, alias), schema, e);
+ }
+ }
+
+ @POST
+ @Path("{model}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "link-model-version." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "link-model-version", absolute = true)
+ public Response linkModelVersion(
+ @PathParam("metalake") String metalake,
+ @PathParam("catalog") String catalog,
+ @PathParam("schema") String schema,
+ @PathParam("model") String model,
+ ModelVersionLinkRequest request) {
+ LOG.info("Received link model version request: {}.{}.{}.{}", metalake,
catalog, schema, model);
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, model);
+
+ try {
+ request.validate();
+
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ modelDispatcher.linkModelVersion(
+ modelId,
+ request.getUri(),
+ request.getAliases(),
+ request.getComment(),
+ request.getProperties());
+ LOG.info("Model version linked: {}", modelId);
+ return Utils.ok(new BaseResponse());
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleModelException(OperationType.LINK, model,
schema, e);
+ }
+ }
+
+ @DELETE
+ @Path("{model}/versions/{version}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "delete-model-version." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "delete-model-version", absolute = true)
+ public Response deleteModelVersion(
+ @PathParam("metalake") String metalake,
+ @PathParam("catalog") String catalog,
+ @PathParam("schema") String schema,
+ @PathParam("model") String model,
+ @PathParam("version") int version) {
+ LOG.info(
+ "Received delete model version request: {}.{}.{}.{}.{}",
+ metalake,
+ catalog,
+ schema,
+ model,
+ version);
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, model);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ boolean deleted = modelDispatcher.deleteModelVersion(modelId,
version);
+ if (!deleted) {
+ LOG.warn("Cannot find to be deleted version {} in model {}",
version, model);
+ } else {
+ LOG.info("Model version deleted: {}.{}", modelId, version);
+ }
+
+ return Utils.ok(new DropResponse(deleted));
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleModelException(
+ OperationType.DELETE, versionString(model, version), schema, e);
+ }
+ }
+
+ @DELETE
+ @Path("{model}/aliases/{alias}")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "delete-model-alias." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "delete-model-alias", absolute = true)
+ public Response deleteModelVersionByAlias(
+ @PathParam("metalake") String metalake,
+ @PathParam("catalog") String catalog,
+ @PathParam("schema") String schema,
+ @PathParam("model") String model,
+ @PathParam("alias") String alias) {
+ LOG.info(
+ "Received delete model version by alias request: {}.{}.{}.{}.{}",
+ metalake,
+ catalog,
+ schema,
+ model,
+ alias);
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, model);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ boolean deleted = modelDispatcher.deleteModelVersion(modelId,
alias);
+ if (!deleted) {
+ LOG.warn(
+ "Cannot find to be deleted model version by alias {} in
model {}", alias, model);
+ } else {
+ LOG.info("Model version by alias deleted: {}.{}", modelId,
alias);
+ }
+
+ return Utils.ok(new DropResponse(deleted));
+ });
+
+ } catch (Exception e) {
+ return ExceptionHandlers.handleModelException(
+ OperationType.DELETE, aliasString(model, alias), schema, e);
+ }
+ }
+
+ private String versionString(String model, int version) {
+ return model + " version(" + version + ")";
+ }
+
+ private String aliasString(String model, String alias) {
+ return model + " alias(" + alias + ")";
+ }
+}
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 8d4bc322ae..2b8abd91f1 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
@@ -35,4 +35,7 @@ public enum OperationType {
REVOKE,
ASSOCIATE,
SET,
+ REGISTER, // An operation to register a model
+ LIST_VERSIONS, // An operation to list versions of a model
+ LINK // An operation to link a version to a model
}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java
new file mode 100644
index 0000000000..42e48d0302
--- /dev/null
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java
@@ -0,0 +1,843 @@
+/*
+ * 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.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Map;
+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.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.catalog.ModelDispatcher;
+import org.apache.gravitino.dto.requests.ModelRegisterRequest;
+import org.apache.gravitino.dto.requests.ModelVersionLinkRequest;
+import org.apache.gravitino.dto.responses.BaseResponse;
+import org.apache.gravitino.dto.responses.DropResponse;
+import org.apache.gravitino.dto.responses.EntityListResponse;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.dto.responses.ModelResponse;
+import org.apache.gravitino.dto.responses.ModelVersionListResponse;
+import org.apache.gravitino.dto.responses.ModelVersionResponse;
+import org.apache.gravitino.exceptions.ModelAlreadyExistsException;
+import org.apache.gravitino.exceptions.NoSuchModelException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.model.ModelVersion;
+import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.gravitino.utils.NamespaceUtil;
+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 TestModelOperations 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 ModelDispatcher modelDispatcher = mock(ModelDispatcher.class);
+
+ private AuditInfo testAuditInfo =
+
AuditInfo.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+
+ private Map<String, String> properties = ImmutableMap.of("key1", "value");
+
+ private String metalake = "metalake_for_model_test";
+
+ private String catalog = "catalog_for_model_test";
+
+ private String schema = "schema_for_model_test";
+
+ private Namespace modelNs = NamespaceUtil.ofModel(metalake, catalog, schema);
+
+ @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(ModelOperations.class);
+ resourceConfig.register(
+ new AbstractBinder() {
+ @Override
+ protected void configure() {
+ bind(modelDispatcher).to(ModelDispatcher.class).ranked(2);
+ bindFactory(TestModelOperations.MockServletRequestFactory.class)
+ .to(HttpServletRequest.class);
+ }
+ });
+
+ return resourceConfig;
+ }
+
+ @Test
+ public void testListModels() {
+ NameIdentifier modelId1 = NameIdentifierUtil.ofModel(metalake, catalog,
schema, "model1");
+ NameIdentifier modelId2 = NameIdentifierUtil.ofModel(metalake, catalog,
schema, "model2");
+ NameIdentifier[] modelIds = new NameIdentifier[] {modelId1, modelId2};
+ when(modelDispatcher.listModels(modelNs)).thenReturn(modelIds);
+
+ Response response =
+ target(modelPath())
+ .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());
+
+ EntityListResponse resp = response.readEntity(EntityListResponse.class);
+ Assertions.assertEquals(0, resp.getCode());
+ Assertions.assertArrayEquals(modelIds, resp.identifiers());
+
+ // Test mock return null for listModels
+ when(modelDispatcher.listModels(modelNs)).thenReturn(null);
+ Response resp1 =
+ target(modelPath())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp1.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp1.getMediaType());
+
+ EntityListResponse resp2 = resp1.readEntity(EntityListResponse.class);
+ Assertions.assertEquals(0, resp2.getCode());
+ Assertions.assertEquals(0, resp2.identifiers().length);
+
+ // Test mock return empty array for listModels
+ when(modelDispatcher.listModels(modelNs)).thenReturn(new
NameIdentifier[0]);
+ Response resp3 =
+ target(modelPath())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp3.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp3.getMediaType());
+
+ EntityListResponse resp4 = resp3.readEntity(EntityListResponse.class);
+ Assertions.assertEquals(0, resp4.getCode());
+ Assertions.assertEquals(0, resp4.identifiers().length);
+
+ // Test mock throw NoSuchSchemaException
+ doThrow(new NoSuchSchemaException("mock
error")).when(modelDispatcher).listModels(modelNs);
+ Response resp5 =
+ target(modelPath())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
resp5.getStatus());
+
+ ErrorResponse errorResp = resp5.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResp.getCode());
+ Assertions.assertEquals(NoSuchSchemaException.class.getSimpleName(),
errorResp.getType());
+
+ // Test mock throw RuntimeException
+ doThrow(new RuntimeException("mock
error")).when(modelDispatcher).listModels(modelNs);
+ Response resp6 =
+ target(modelPath())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp6.getStatus());
+
+ ErrorResponse errorResp1 = resp6.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp1.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp1.getType());
+ }
+
+ @Test
+ public void testGetModel() {
+ Model mockModel = mockModel("model1", "comment1", 0);
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, "model1");
+ when(modelDispatcher.getModel(modelId)).thenReturn(mockModel);
+
+ Response resp =
+ target(modelPath())
+ .path("model1")
+ .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());
+
+ ModelResponse modelResp = resp.readEntity(ModelResponse.class);
+ Assertions.assertEquals(0, modelResp.getCode());
+
+ Model resultModel = modelResp.getModel();
+ compare(mockModel, resultModel);
+
+ // Test mock throw NoSuchModelException
+ doThrow(new NoSuchModelException("mock
error")).when(modelDispatcher).getModel(modelId);
+ Response resp1 =
+ target(modelPath())
+ .path("model1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ 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(NoSuchModelException.class.getSimpleName(),
errorResp.getType());
+
+ // Test mock throw RuntimeException
+ doThrow(new RuntimeException("mock
error")).when(modelDispatcher).getModel(modelId);
+ Response resp2 =
+ target(modelPath())
+ .path("model1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ 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 testRegisterModel() {
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, "model1");
+ Model mockModel = mockModel("model1", "comment1", 0);
+ when(modelDispatcher.registerModel(modelId, "comment1",
properties)).thenReturn(mockModel);
+
+ ModelRegisterRequest req = new ModelRegisterRequest("model1", "comment1",
properties);
+ Response resp =
+ target(modelPath())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ ModelResponse modelResp = resp.readEntity(ModelResponse.class);
+ Assertions.assertEquals(0, modelResp.getCode());
+ compare(mockModel, modelResp.getModel());
+
+ // Test mock throw NoSuchSchemaException
+ doThrow(new NoSuchSchemaException("mock error"))
+ .when(modelDispatcher)
+ .registerModel(modelId, "comment1", properties);
+
+ Response resp1 =
+ target(modelPath())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, 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(NoSuchSchemaException.class.getSimpleName(),
errorResp.getType());
+
+ // Test mock throw ModelAlreadyExistsException
+ doThrow(new ModelAlreadyExistsException("mock error"))
+ .when(modelDispatcher)
+ .registerModel(modelId, "comment1", properties);
+
+ Response resp2 =
+ target(modelPath())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(),
resp2.getStatus());
+
+ ErrorResponse errorResp1 = resp2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.ALREADY_EXISTS_CODE,
errorResp1.getCode());
+ Assertions.assertEquals(
+ ModelAlreadyExistsException.class.getSimpleName(),
errorResp1.getType());
+
+ // Test mock throw RuntimeException
+ doThrow(new RuntimeException("mock error"))
+ .when(modelDispatcher)
+ .registerModel(modelId, "comment1", properties);
+
+ Response resp3 =
+ target(modelPath())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp3.getStatus());
+
+ ErrorResponse errorResp2 = resp3.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp2.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp2.getType());
+ }
+
+ @Test
+ public void testDeleteModel() {
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, "model1");
+ when(modelDispatcher.deleteModel(modelId)).thenReturn(true);
+
+ Response resp =
+ target(modelPath())
+ .path("model1")
+ .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());
+
+ // Test mock return false for deleteModel
+ when(modelDispatcher.deleteModel(modelId)).thenReturn(false);
+ Response resp1 =
+ target(modelPath())
+ .path("model1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .delete();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp1.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp1.getMediaType());
+
+ DropResponse dropResp1 = resp1.readEntity(DropResponse.class);
+ Assertions.assertEquals(0, dropResp1.getCode());
+ Assertions.assertFalse(dropResp1.dropped());
+
+ // Test mock throw RuntimeException
+ doThrow(new RuntimeException("mock
error")).when(modelDispatcher).deleteModel(modelId);
+ Response resp2 =
+ target(modelPath())
+ .path("model1")
+ .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 testListModelVersions() {
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, "model1");
+ int[] versions = new int[] {0, 1, 2};
+ when(modelDispatcher.listModelVersions(modelId)).thenReturn(versions);
+
+ Response resp =
+ target(modelPath())
+ .path("model1")
+ .path("versions")
+ .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());
+
+ ModelVersionListResponse versionListResp =
resp.readEntity(ModelVersionListResponse.class);
+ Assertions.assertEquals(0, versionListResp.getCode());
+ Assertions.assertArrayEquals(versions, versionListResp.getVersions());
+
+ // Test mock return null for listModelVersions
+ when(modelDispatcher.listModelVersions(modelId)).thenReturn(null);
+ Response resp1 =
+ target(modelPath())
+ .path("model1")
+ .path("versions")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp1.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp1.getMediaType());
+
+ ModelVersionListResponse versionListResp1 =
resp1.readEntity(ModelVersionListResponse.class);
+ Assertions.assertEquals(0, versionListResp1.getCode());
+ Assertions.assertEquals(0, versionListResp1.getVersions().length);
+
+ // Test mock return empty array for listModelVersions
+ when(modelDispatcher.listModelVersions(modelId)).thenReturn(new int[0]);
+ Response resp2 =
+ target(modelPath())
+ .path("model1")
+ .path("versions")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp2.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp2.getMediaType());
+
+ ModelVersionListResponse versionListResp2 =
resp2.readEntity(ModelVersionListResponse.class);
+ Assertions.assertEquals(0, versionListResp2.getCode());
+ Assertions.assertEquals(0, versionListResp2.getVersions().length);
+
+ // Test mock throw NoSuchModelException
+ doThrow(new NoSuchModelException("mock error"))
+ .when(modelDispatcher)
+ .listModelVersions(modelId);
+ Response resp3 =
+ target(modelPath())
+ .path("model1")
+ .path("versions")
+ .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(NoSuchModelException.class.getSimpleName(),
errorResp.getType());
+
+ // Test mock throw RuntimeException
+ doThrow(new RuntimeException("mock
error")).when(modelDispatcher).listModelVersions(modelId);
+ Response resp4 =
+ target(modelPath())
+ .path("model1")
+ .path("versions")
+ .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 testGetModelVersion() {
+ NameIdentifier modelIdent = NameIdentifierUtil.ofModel(metalake, catalog,
schema, "model1");
+ ModelVersion mockModelVersion =
+ mockModelVersion(0, "uri1", new String[] {"alias1"}, "comment1");
+ when(modelDispatcher.getModelVersion(modelIdent,
0)).thenReturn(mockModelVersion);
+
+ Response resp =
+ target(modelPath())
+ .path("model1")
+ .path("versions")
+ .path("0")
+ .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());
+
+ ModelVersionResponse versionResp =
resp.readEntity(ModelVersionResponse.class);
+ Assertions.assertEquals(0, versionResp.getCode());
+ compare(mockModelVersion, versionResp.getModelVersion());
+
+ // Test mock throw NoSuchModelVersionException
+ doThrow(new NoSuchModelException("mock error"))
+ .when(modelDispatcher)
+ .getModelVersion(modelIdent, 0);
+
+ Response resp1 =
+ target(modelPath())
+ .path("model1")
+ .path("versions")
+ .path("0")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ 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(NoSuchModelException.class.getSimpleName(),
errorResp.getType());
+
+ // Test mock throw RuntimeException
+ doThrow(new RuntimeException("mock error"))
+ .when(modelDispatcher)
+ .getModelVersion(modelIdent, 0);
+
+ Response resp2 =
+ target(modelPath())
+ .path("model1")
+ .path("versions")
+ .path("0")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ 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 get model version by alias
+ when(modelDispatcher.getModelVersion(modelIdent,
"alias1")).thenReturn(mockModelVersion);
+
+ Response resp3 =
+ target(modelPath())
+ .path("model1")
+ .path("aliases")
+ .path("alias1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp3.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp3.getMediaType());
+
+ ModelVersionResponse versionResp1 =
resp3.readEntity(ModelVersionResponse.class);
+ Assertions.assertEquals(0, versionResp1.getCode());
+ compare(mockModelVersion, versionResp1.getModelVersion());
+
+ // Test mock throw NoSuchModelVersionException
+ doThrow(new NoSuchModelException("mock error"))
+ .when(modelDispatcher)
+ .getModelVersion(modelIdent, "alias1");
+
+ Response resp4 =
+ target(modelPath())
+ .path("model1")
+ .path("aliases")
+ .path("alias1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
resp4.getStatus());
+
+ ErrorResponse errorResp2 = resp4.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResp2.getCode());
+ Assertions.assertEquals(NoSuchModelException.class.getSimpleName(),
errorResp2.getType());
+
+ // Test mock throw RuntimeException
+ doThrow(new RuntimeException("mock error"))
+ .when(modelDispatcher)
+ .getModelVersion(modelIdent, "alias1");
+
+ Response resp5 =
+ target(modelPath())
+ .path("model1")
+ .path("aliases")
+ .path("alias1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp5.getStatus());
+
+ ErrorResponse errorResp3 = resp5.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp3.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp3.getType());
+ }
+
+ @Test
+ public void testLinkModelVersion() {
+ NameIdentifier modelIdent = NameIdentifierUtil.ofModel(metalake, catalog,
schema, "model1");
+ doNothing()
+ .when(modelDispatcher)
+ .linkModelVersion(modelIdent, "uri1", new String[] {"alias1"},
"comment1", properties);
+
+ ModelVersionLinkRequest req =
+ new ModelVersionLinkRequest("uri1", new String[] {"alias1"},
"comment1", properties);
+
+ Response resp =
+ target(modelPath())
+ .path("model1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ BaseResponse baseResponse = resp.readEntity(BaseResponse.class);
+ Assertions.assertEquals(0, baseResponse.getCode());
+
+ // Test mock throw NoSuchModelException
+ doThrow(new NoSuchModelException("mock error"))
+ .when(modelDispatcher)
+ .linkModelVersion(modelIdent, "uri1", new String[] {"alias1"},
"comment1", properties);
+
+ Response resp1 =
+ target(modelPath())
+ .path("model1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, 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(NoSuchModelException.class.getSimpleName(),
errorResp.getType());
+
+ // Test mock throw ModelVersionAliasesAlreadyExistException
+ doThrow(new ModelAlreadyExistsException("mock error"))
+ .when(modelDispatcher)
+ .linkModelVersion(modelIdent, "uri1", new String[] {"alias1"},
"comment1", properties);
+
+ Response resp2 =
+ target(modelPath())
+ .path("model1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(),
resp2.getStatus());
+
+ ErrorResponse errorResp1 = resp2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.ALREADY_EXISTS_CODE,
errorResp1.getCode());
+ Assertions.assertEquals(
+ ModelAlreadyExistsException.class.getSimpleName(),
errorResp1.getType());
+
+ // Test mock throw RuntimeException
+ doThrow(new RuntimeException("mock error"))
+ .when(modelDispatcher)
+ .linkModelVersion(modelIdent, "uri1", new String[] {"alias1"},
"comment1", properties);
+
+ Response resp3 =
+ target(modelPath())
+ .path("model1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp3.getStatus());
+
+ ErrorResponse errorResp2 = resp3.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp2.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp2.getType());
+ }
+
+ @Test
+ public void testDeleteModelVersion() {
+ NameIdentifier modelIdent = NameIdentifierUtil.ofModel(metalake, catalog,
schema, "model1");
+ when(modelDispatcher.deleteModelVersion(modelIdent, 0)).thenReturn(true);
+
+ Response resp =
+ target(modelPath())
+ .path("model1")
+ .path("versions")
+ .path("0")
+ .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());
+
+ // Test mock return false for deleteModelVersion
+ when(modelDispatcher.deleteModelVersion(modelIdent, 0)).thenReturn(false);
+
+ Response resp1 =
+ target(modelPath())
+ .path("model1")
+ .path("versions")
+ .path("0")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .delete();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp1.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp1.getMediaType());
+
+ DropResponse dropResp1 = resp1.readEntity(DropResponse.class);
+ Assertions.assertEquals(0, dropResp1.getCode());
+ Assertions.assertFalse(dropResp1.dropped());
+
+ // Test mock return true for deleteModelVersion using alias
+ when(modelDispatcher.deleteModelVersion(modelIdent,
"alias1")).thenReturn(true);
+
+ Response resp2 =
+ target(modelPath())
+ .path("model1")
+ .path("aliases")
+ .path("alias1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .delete();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp2.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp2.getMediaType());
+
+ DropResponse dropResp2 = resp2.readEntity(DropResponse.class);
+ Assertions.assertEquals(0, dropResp2.getCode());
+
+ // Test mock return false for deleteModelVersion using alias
+ when(modelDispatcher.deleteModelVersion(modelIdent,
"alias1")).thenReturn(false);
+
+ Response resp3 =
+ target(modelPath())
+ .path("model1")
+ .path("aliases")
+ .path("alias1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .delete();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp3.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp3.getMediaType());
+
+ DropResponse dropResp3 = resp3.readEntity(DropResponse.class);
+ Assertions.assertEquals(0, dropResp3.getCode());
+ Assertions.assertFalse(dropResp3.dropped());
+
+ // Test mock throw RuntimeException
+ doThrow(new RuntimeException("mock error"))
+ .when(modelDispatcher)
+ .deleteModelVersion(modelIdent, 0);
+
+ Response resp4 =
+ target(modelPath())
+ .path("model1")
+ .path("versions")
+ .path("0")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .delete();
+
+ 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 mock throw RuntimeException using alias
+ doThrow(new RuntimeException("mock error"))
+ .when(modelDispatcher)
+ .deleteModelVersion(modelIdent, "alias1");
+
+ Response resp5 =
+ target(modelPath())
+ .path("model1")
+ .path("aliases")
+ .path("alias1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .delete();
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp5.getStatus());
+
+ ErrorResponse errorResp2 = resp5.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp2.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp2.getType());
+ }
+
+ private String modelPath() {
+ return "/metalakes/" + metalake + "/catalogs/" + catalog + "/schemas/" +
schema + "/models";
+ }
+
+ private Model mockModel(String modelName, String comment, int latestVersion)
{
+ Model mockModel = mock(Model.class);
+ when(mockModel.name()).thenReturn(modelName);
+ when(mockModel.comment()).thenReturn(comment);
+ when(mockModel.latestVersion()).thenReturn(latestVersion);
+ when(mockModel.properties()).thenReturn(properties);
+ when(mockModel.auditInfo()).thenReturn(testAuditInfo);
+ return mockModel;
+ }
+
+ private ModelVersion mockModelVersion(int version, String uri, String[]
aliases, String comment) {
+ ModelVersion mockModelVersion = mock(ModelVersion.class);
+ when(mockModelVersion.version()).thenReturn(version);
+ when(mockModelVersion.uri()).thenReturn(uri);
+ when(mockModelVersion.aliases()).thenReturn(aliases);
+ when(mockModelVersion.comment()).thenReturn(comment);
+ when(mockModelVersion.properties()).thenReturn(properties);
+ when(mockModelVersion.auditInfo()).thenReturn(testAuditInfo);
+ return mockModelVersion;
+ }
+
+ private void compare(Model left, Model right) {
+ Assertions.assertEquals(left.name(), right.name());
+ Assertions.assertEquals(left.comment(), right.comment());
+ Assertions.assertEquals(left.properties(), right.properties());
+
+ Assertions.assertNotNull(right.auditInfo());
+ Assertions.assertEquals(left.auditInfo().creator(),
right.auditInfo().creator());
+ Assertions.assertEquals(left.auditInfo().createTime(),
right.auditInfo().createTime());
+ Assertions.assertEquals(left.auditInfo().lastModifier(),
right.auditInfo().lastModifier());
+ Assertions.assertEquals(
+ left.auditInfo().lastModifiedTime(),
right.auditInfo().lastModifiedTime());
+ }
+
+ private void compare(ModelVersion left, ModelVersion right) {
+ Assertions.assertEquals(left.version(), right.version());
+ Assertions.assertEquals(left.uri(), right.uri());
+ Assertions.assertArrayEquals(left.aliases(), right.aliases());
+ Assertions.assertEquals(left.comment(), right.comment());
+ Assertions.assertEquals(left.properties(), right.properties());
+
+ Assertions.assertNotNull(right.auditInfo());
+ Assertions.assertEquals(left.auditInfo().creator(),
right.auditInfo().creator());
+ Assertions.assertEquals(left.auditInfo().createTime(),
right.auditInfo().createTime());
+ Assertions.assertEquals(left.auditInfo().lastModifier(),
right.auditInfo().lastModifier());
+ Assertions.assertEquals(
+ left.auditInfo().lastModifiedTime(),
right.auditInfo().lastModifiedTime());
+ }
+}