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

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


The following commit(s) were added to refs/heads/branch-1.0 by this push:
     new 6dc8135b26 [#8640] feat(server): Add server-side REST interface for 
job template alteration (#8790)
6dc8135b26 is described below

commit 6dc8135b26a233c32640572b305999d5dbb5861c
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Oct 11 19:19:34 2025 +0800

    [#8640] feat(server): Add server-side REST interface for job template 
alteration (#8790)
    
    ### What changes were proposed in this pull request?
    
    This PR adds the server-side REST interface for job template alteration.
    
    ### Why are the changes needed?
    
    This is a part of work to support job template alteration.
    
    Fix: #8640
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    UTs added.
    
    Co-authored-by: Jerry Shao <[email protected]>
---
 .../gravitino/dto/job/ShellTemplateUpdateDTO.java  |  60 ++++
 .../gravitino/dto/job/SparkTemplateUpdateDTO.java  |  81 ++++++
 .../gravitino/dto/job/TemplateUpdateDTO.java       |  77 +++++
 .../dto/requests/JobTemplateUpdateRequest.java     | 163 +++++++++++
 .../dto/requests/JobTemplateUpdatesRequest.java    |  60 ++++
 .../gravitino/dto/job/TestTemplateUpdateDTO.java   | 320 +++++++++++++++++++++
 .../requests/TestJobTemplateUpdatesRequest.java    |  53 ++++
 .../gravitino/server/web/rest/JobOperations.java   |  38 +++
 .../server/web/rest/TestJobOperations.java         | 129 +++++++++
 9 files changed, 981 insertions(+)

diff --git 
a/common/src/main/java/org/apache/gravitino/dto/job/ShellTemplateUpdateDTO.java 
b/common/src/main/java/org/apache/gravitino/dto/job/ShellTemplateUpdateDTO.java
new file mode 100644
index 0000000000..23574c6e36
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/job/ShellTemplateUpdateDTO.java
@@ -0,0 +1,60 @@
+/*
+ * 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.job;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import javax.annotation.Nullable;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.experimental.SuperBuilder;
+import org.apache.gravitino.job.JobTemplateChange;
+
+/** DTO for updating a Shell Job Template. */
+@Getter
+@EqualsAndHashCode(callSuper = true)
+@SuperBuilder(setterPrefix = "with")
+@ToString(callSuper = true)
+public class ShellTemplateUpdateDTO extends TemplateUpdateDTO {
+
+  @Nullable
+  @JsonProperty("newScripts")
+  private List<String> newScripts;
+
+  /**
+   * The default constructor for Jackson. This constructor is required for 
deserialization of the
+   * DTO.
+   */
+  private ShellTemplateUpdateDTO() {
+    // Default constructor for Jackson
+    super();
+  }
+
+  @Override
+  public JobTemplateChange.TemplateUpdate toTemplateUpdate() {
+    return JobTemplateChange.ShellTemplateUpdate.builder()
+        .withNewExecutable(getNewExecutable())
+        .withNewArguments(getNewArguments())
+        .withNewEnvironments(getNewEnvironments())
+        .withNewCustomFields(getNewCustomFields())
+        .withNewScripts(newScripts)
+        .build();
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/job/SparkTemplateUpdateDTO.java 
b/common/src/main/java/org/apache/gravitino/dto/job/SparkTemplateUpdateDTO.java
new file mode 100644
index 0000000000..5ef5027331
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/job/SparkTemplateUpdateDTO.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.job;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.experimental.SuperBuilder;
+import org.apache.gravitino.job.JobTemplateChange;
+
+/** DTO for updating a Spark Job Template. */
+@Getter
+@EqualsAndHashCode(callSuper = true)
+@SuperBuilder(setterPrefix = "with")
+@ToString(callSuper = true)
+public class SparkTemplateUpdateDTO extends TemplateUpdateDTO {
+
+  @Nullable
+  @JsonProperty("newClassName")
+  private String newClassName;
+
+  @Nullable
+  @JsonProperty("newJars")
+  private List<String> newJars;
+
+  @Nullable
+  @JsonProperty("newFiles")
+  private List<String> newFiles;
+
+  @Nullable
+  @JsonProperty("newArchives")
+  private List<String> newArchives;
+
+  @Nullable
+  @JsonProperty("newConfigs")
+  private Map<String, String> newConfigs;
+
+  /**
+   * The default constructor for Jackson. This constructor is required for 
deserialization of the
+   * DTO.
+   */
+  private SparkTemplateUpdateDTO() {
+    // Default constructor for Jackson
+    super();
+  }
+
+  @Override
+  public JobTemplateChange.TemplateUpdate toTemplateUpdate() {
+    return JobTemplateChange.SparkTemplateUpdate.builder()
+        .withNewExecutable(getNewExecutable())
+        .withNewArguments(getNewArguments())
+        .withNewEnvironments(getNewEnvironments())
+        .withNewCustomFields(getNewCustomFields())
+        .withNewClassName(newClassName)
+        .withNewJars(newJars)
+        .withNewFiles(newFiles)
+        .withNewArchives(newArchives)
+        .withNewConfigs(newConfigs)
+        .build();
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/job/TemplateUpdateDTO.java 
b/common/src/main/java/org/apache/gravitino/dto/job/TemplateUpdateDTO.java
new file mode 100644
index 0000000000..8cda83393b
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/job/TemplateUpdateDTO.java
@@ -0,0 +1,77 @@
+/*
+ * 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.job;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.experimental.SuperBuilder;
+import org.apache.gravitino.job.JobTemplateChange;
+
+/** DTO for updating a Job Template. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
+@JsonSubTypes({
+  @JsonSubTypes.Type(value = ShellTemplateUpdateDTO.class, name = "shell"),
+  @JsonSubTypes.Type(value = SparkTemplateUpdateDTO.class, name = "spark")
+})
+@Getter
+@EqualsAndHashCode
+@SuperBuilder(setterPrefix = "with")
+@ToString
+public abstract class TemplateUpdateDTO {
+
+  @Nullable
+  @JsonProperty("newExecutable")
+  private String newExecutable;
+
+  @Nullable
+  @JsonProperty("newArguments")
+  private List<String> newArguments;
+
+  @Nullable
+  @JsonProperty("newEnvironments")
+  private Map<String, String> newEnvironments;
+
+  @Nullable
+  @JsonProperty("newCustomFields")
+  private Map<String, String> newCustomFields;
+
+  /**
+   * The default constructor for Jackson. This constructor is required for 
deserialization of the
+   * DTO.
+   */
+  protected TemplateUpdateDTO() {
+    // Default constructor for Jackson
+  }
+
+  /**
+   * Converts this DTO to a TemplateUpdate object.
+   *
+   * @return the corresponding TemplateUpdate object
+   */
+  public abstract JobTemplateChange.TemplateUpdate toTemplateUpdate();
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/JobTemplateUpdateRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/JobTemplateUpdateRequest.java
new file mode 100644
index 0000000000..5b513c6260
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/JobTemplateUpdateRequest.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.requests;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.google.common.base.Preconditions;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.dto.job.TemplateUpdateDTO;
+import org.apache.gravitino.job.JobTemplateChange;
+import org.apache.gravitino.rest.RESTRequest;
+
+/**
+ * Represents a request to update a job template. This can include renaming 
the template, updating
+ * its comment, or changing its content.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
+@JsonSubTypes({
+  @JsonSubTypes.Type(
+      value = JobTemplateUpdateRequest.RenameJobTemplateRequest.class,
+      name = "rename"),
+  @JsonSubTypes.Type(
+      value = JobTemplateUpdateRequest.UpdateJobTemplateCommentRequest.class,
+      name = "updateComment"),
+  @JsonSubTypes.Type(
+      value = JobTemplateUpdateRequest.UpdateJobTemplateContentRequest.class,
+      name = "updateTemplate")
+})
+public interface JobTemplateUpdateRequest extends RESTRequest {
+
+  /**
+   * Get the job template change that is requested.
+   *
+   * @return the job template change
+   */
+  JobTemplateChange jobTemplateChange();
+
+  /** The request to rename a job template. */
+  @EqualsAndHashCode
+  @ToString
+  class RenameJobTemplateRequest implements JobTemplateUpdateRequest {
+
+    @Getter
+    @JsonProperty("newName")
+    private final String newName;
+
+    /**
+     * Constructor for RenameJobTemplateRequest.
+     *
+     * @param newName the new name for the job template
+     */
+    public RenameJobTemplateRequest(String newName) {
+      this.newName = newName;
+    }
+
+    /** Default constructor for Jackson. */
+    private RenameJobTemplateRequest() {
+      this(null);
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(newName), "\"newName\" is required and cannot 
be empty");
+    }
+
+    @Override
+    public JobTemplateChange jobTemplateChange() {
+      return JobTemplateChange.rename(newName);
+    }
+  }
+
+  /** The request to update the comment of a job template. */
+  @EqualsAndHashCode
+  @ToString
+  class UpdateJobTemplateCommentRequest implements JobTemplateUpdateRequest {
+
+    @Getter
+    @JsonProperty("newComment")
+    private final String newComment;
+
+    /**
+     * Constructor for UpdateJobTemplateCommentRequest.
+     *
+     * @param newComment the new comment for the job template
+     */
+    public UpdateJobTemplateCommentRequest(String newComment) {
+      this.newComment = newComment;
+    }
+
+    /** Default constructor for Jackson. */
+    private UpdateJobTemplateCommentRequest() {
+      this(null);
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      // No specific validation needed for comment, it can be null or empty
+    }
+
+    @Override
+    public JobTemplateChange jobTemplateChange() {
+      return JobTemplateChange.updateComment(newComment);
+    }
+  }
+
+  /** The request to update the content of a job template. */
+  @EqualsAndHashCode
+  @ToString
+  class UpdateJobTemplateContentRequest implements JobTemplateUpdateRequest {
+
+    @Getter
+    @JsonProperty("newTemplate")
+    private final TemplateUpdateDTO newTemplate;
+
+    /**
+     * Constructor for UpdateJobTemplateContentRequest.
+     *
+     * @param newTemplate the new template content for the job template
+     */
+    public UpdateJobTemplateContentRequest(TemplateUpdateDTO newTemplate) {
+      this.newTemplate = newTemplate;
+    }
+
+    /** Default constructor for Jackson. */
+    private UpdateJobTemplateContentRequest() {
+      this(null);
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(
+          newTemplate != null, "\"newTemplate\" is required and cannot be 
null");
+    }
+
+    @Override
+    public JobTemplateChange jobTemplateChange() {
+      return JobTemplateChange.updateTemplate(newTemplate.toTemplateUpdate());
+    }
+  }
+}
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/JobTemplateUpdatesRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/JobTemplateUpdatesRequest.java
new file mode 100644
index 0000000000..fc3b12c5d6
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/JobTemplateUpdatesRequest.java
@@ -0,0 +1,60 @@
+/*
+ * 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 java.util.List;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to update a job template with series of updates. */
+@Getter
+@EqualsAndHashCode
+@ToString
+public class JobTemplateUpdatesRequest implements RESTRequest {
+
+  @JsonProperty("updates")
+  private final List<JobTemplateUpdateRequest> updates;
+
+  /**
+   * Creates a new JobTemplateUpdatesRequest.
+   *
+   * @param updates The updates to apply to the job template.
+   */
+  public JobTemplateUpdatesRequest(List<JobTemplateUpdateRequest> updates) {
+    this.updates = updates;
+  }
+
+  /** This is the constructor that is used by Jackson deserializer */
+  private JobTemplateUpdatesRequest() {
+    this(null);
+  }
+
+  /**
+   * Validates the request.
+   *
+   * @throws IllegalArgumentException If the request is invalid, this 
exception is thrown.
+   */
+  @Override
+  public void validate() throws IllegalArgumentException {
+    updates.forEach(RESTRequest::validate);
+  }
+}
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/job/TestTemplateUpdateDTO.java 
b/common/src/test/java/org/apache/gravitino/dto/job/TestTemplateUpdateDTO.java
new file mode 100644
index 0000000000..16235f0b45
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/dto/job/TestTemplateUpdateDTO.java
@@ -0,0 +1,320 @@
+/*
+ * 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.job;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTemplateUpdateDTO {
+
+  @Test
+  public void testShellTemplateUpdateDTO() throws JsonProcessingException {
+    ShellTemplateUpdateDTO shellTemplateUpdateDTO =
+        ShellTemplateUpdateDTO.builder()
+            .withNewExecutable("/bin/bash")
+            .withNewArguments(ImmutableList.of("-c", "echo Hello World"))
+            .withNewEnvironments(ImmutableMap.of("ENV_VAR", "value"))
+            .withNewCustomFields(ImmutableMap.of("customKey", "customValue"))
+            .withNewScripts(ImmutableList.of("/path/to/script1.sh", 
"/path/to/script2.sh"))
+            .build();
+
+    String serJson = 
JsonUtils.objectMapper().writeValueAsString(shellTemplateUpdateDTO);
+    ShellTemplateUpdateDTO deserDTO =
+        JsonUtils.objectMapper().readValue(serJson, 
ShellTemplateUpdateDTO.class);
+    Assertions.assertEquals(shellTemplateUpdateDTO, deserDTO);
+
+    shellTemplateUpdateDTO =
+        ShellTemplateUpdateDTO.builder()
+            .withNewEnvironments(ImmutableMap.of("ENV_VAR", "value"))
+            .withNewCustomFields(ImmutableMap.of("customKey", "customValue"))
+            .withNewScripts(ImmutableList.of("/path/to/script1.sh", 
"/path/to/script2.sh"))
+            .build();
+
+    serJson = 
JsonUtils.objectMapper().writeValueAsString(shellTemplateUpdateDTO);
+    deserDTO = JsonUtils.objectMapper().readValue(serJson, 
ShellTemplateUpdateDTO.class);
+    Assertions.assertNull(deserDTO.getNewExecutable());
+    Assertions.assertNull(deserDTO.getNewArguments());
+    Assertions.assertEquals(shellTemplateUpdateDTO, deserDTO);
+
+    shellTemplateUpdateDTO =
+        ShellTemplateUpdateDTO.builder()
+            .withNewScripts(ImmutableList.of("/path/to/script1.sh", 
"/path/to/script2.sh"))
+            .build();
+
+    serJson = 
JsonUtils.objectMapper().writeValueAsString(shellTemplateUpdateDTO);
+    deserDTO = JsonUtils.objectMapper().readValue(serJson, 
ShellTemplateUpdateDTO.class);
+    Assertions.assertNull(deserDTO.getNewEnvironments());
+    Assertions.assertNull(deserDTO.getNewCustomFields());
+    Assertions.assertEquals(shellTemplateUpdateDTO, deserDTO);
+
+    shellTemplateUpdateDTO = ShellTemplateUpdateDTO.builder().build();
+    serJson = 
JsonUtils.objectMapper().writeValueAsString(shellTemplateUpdateDTO);
+    deserDTO = JsonUtils.objectMapper().readValue(serJson, 
ShellTemplateUpdateDTO.class);
+    Assertions.assertNull(deserDTO.getNewScripts());
+    Assertions.assertEquals(shellTemplateUpdateDTO, deserDTO);
+  }
+
+  @Test
+  public void testSparkTemplateUpdateDTO() throws JsonProcessingException {
+    SparkTemplateUpdateDTO sparkTemplateUpdateDTO =
+        SparkTemplateUpdateDTO.builder()
+            .withNewExecutable("spark-submit")
+            .withNewArguments(ImmutableList.of("--class", "org.example.Main"))
+            .withNewEnvironments(ImmutableMap.of("SPARK_ENV", "prod"))
+            .withNewCustomFields(ImmutableMap.of("customKey", "customValue"))
+            .withNewClassName("org.example.Main")
+            .withNewJars(ImmutableList.of("/path/to/jar1.jar", 
"/path/to/jar2.jar"))
+            .withNewFiles(ImmutableList.of("/path/to/file1.txt", 
"/path/to/file2.txt"))
+            .withNewArchives(ImmutableList.of("/path/to/archive1.zip", 
"/path/to/archive2.zip"))
+            .withNewConfigs(ImmutableMap.of("spark.executor.memory", "4g"))
+            .build();
+
+    String serJson = 
JsonUtils.objectMapper().writeValueAsString(sparkTemplateUpdateDTO);
+    SparkTemplateUpdateDTO deserDTO =
+        JsonUtils.objectMapper().readValue(serJson, 
SparkTemplateUpdateDTO.class);
+    Assertions.assertEquals(sparkTemplateUpdateDTO, deserDTO);
+
+    sparkTemplateUpdateDTO =
+        SparkTemplateUpdateDTO.builder()
+            .withNewEnvironments(ImmutableMap.of("SPARK_ENV", "prod"))
+            .withNewCustomFields(ImmutableMap.of("customKey", "customValue"))
+            .withNewClassName("org.example.Main")
+            .withNewJars(ImmutableList.of("/path/to/jar1.jar", 
"/path/to/jar2.jar"))
+            .withNewFiles(ImmutableList.of("/path/to/file1.txt", 
"/path/to/file2.txt"))
+            .withNewArchives(ImmutableList.of("/path/to/archive1.zip", 
"/path/to/archive2.zip"))
+            .withNewConfigs(ImmutableMap.of("spark.executor.memory", "4g"))
+            .build();
+
+    serJson = 
JsonUtils.objectMapper().writeValueAsString(sparkTemplateUpdateDTO);
+    deserDTO = JsonUtils.objectMapper().readValue(serJson, 
SparkTemplateUpdateDTO.class);
+    Assertions.assertNull(deserDTO.getNewExecutable());
+    Assertions.assertNull(deserDTO.getNewArguments());
+    Assertions.assertEquals(sparkTemplateUpdateDTO, deserDTO);
+
+    sparkTemplateUpdateDTO =
+        SparkTemplateUpdateDTO.builder()
+            .withNewClassName("org.example.Main")
+            .withNewJars(ImmutableList.of("/path/to/jar1.jar", 
"/path/to/jar2.jar"))
+            .withNewFiles(ImmutableList.of("/path/to/file1.txt", 
"/path/to/file2.txt"))
+            .withNewArchives(ImmutableList.of("/path/to/archive1.zip", 
"/path/to/archive2.zip"))
+            .withNewConfigs(ImmutableMap.of("spark.executor.memory", "4g"))
+            .build();
+
+    serJson = 
JsonUtils.objectMapper().writeValueAsString(sparkTemplateUpdateDTO);
+    deserDTO = JsonUtils.objectMapper().readValue(serJson, 
SparkTemplateUpdateDTO.class);
+    Assertions.assertNull(deserDTO.getNewEnvironments());
+    Assertions.assertNull(deserDTO.getNewCustomFields());
+    Assertions.assertEquals(sparkTemplateUpdateDTO, deserDTO);
+
+    sparkTemplateUpdateDTO =
+        SparkTemplateUpdateDTO.builder()
+            .withNewConfigs(ImmutableMap.of("spark.executor.memory", "4g"))
+            .build();
+
+    serJson = 
JsonUtils.objectMapper().writeValueAsString(sparkTemplateUpdateDTO);
+    deserDTO = JsonUtils.objectMapper().readValue(serJson, 
SparkTemplateUpdateDTO.class);
+    Assertions.assertNull(deserDTO.getNewClassName());
+    Assertions.assertNull(deserDTO.getNewJars());
+    Assertions.assertNull(deserDTO.getNewFiles());
+    Assertions.assertNull(deserDTO.getNewArchives());
+    Assertions.assertEquals(sparkTemplateUpdateDTO, deserDTO);
+
+    sparkTemplateUpdateDTO = SparkTemplateUpdateDTO.builder().build();
+    serJson = 
JsonUtils.objectMapper().writeValueAsString(sparkTemplateUpdateDTO);
+    deserDTO = JsonUtils.objectMapper().readValue(serJson, 
SparkTemplateUpdateDTO.class);
+    Assertions.assertNull(deserDTO.getNewConfigs());
+    Assertions.assertEquals(sparkTemplateUpdateDTO, deserDTO);
+  }
+
+  @Test
+  public void testDeserializeShellTemplateUpdate() throws 
JsonProcessingException {
+    String json =
+        "{"
+            + "\"@type\":\"shell\","
+            + "\"newExecutable\":\"/bin/bash\","
+            + "\"newArguments\":[\"-c\",\"echo Hello World\"],"
+            + "\"newEnvironments\":{\"ENV_VAR\":\"value\"},"
+            + "\"newCustomFields\":{\"customKey\":\"customValue\"},"
+            + 
"\"newScripts\":[\"/path/to/script1.sh\",\"/path/to/script2.sh\"]"
+            + "}";
+
+    TemplateUpdateDTO dto = JsonUtils.objectMapper().readValue(json, 
TemplateUpdateDTO.class);
+    Assertions.assertInstanceOf(ShellTemplateUpdateDTO.class, dto);
+    ShellTemplateUpdateDTO shellDto = (ShellTemplateUpdateDTO) dto;
+    Assertions.assertEquals("/bin/bash", shellDto.getNewExecutable());
+    Assertions.assertEquals(ImmutableList.of("-c", "echo Hello World"), 
shellDto.getNewArguments());
+    Assertions.assertEquals(ImmutableMap.of("ENV_VAR", "value"), 
shellDto.getNewEnvironments());
+    Assertions.assertEquals(
+        ImmutableMap.of("customKey", "customValue"), 
shellDto.getNewCustomFields());
+    Assertions.assertEquals(
+        ImmutableList.of("/path/to/script1.sh", "/path/to/script2.sh"), 
shellDto.getNewScripts());
+
+    json =
+        "{"
+            + "\"@type\":\"shell\","
+            + "\"newEnvironments\":{\"ENV_VAR\":\"value\"},"
+            + "\"newCustomFields\":{\"customKey\":\"customValue\"},"
+            + 
"\"newScripts\":[\"/path/to/script1.sh\",\"/path/to/script2.sh\"]"
+            + "}";
+
+    dto = JsonUtils.objectMapper().readValue(json, TemplateUpdateDTO.class);
+    Assertions.assertInstanceOf(ShellTemplateUpdateDTO.class, dto);
+    shellDto = (ShellTemplateUpdateDTO) dto;
+    Assertions.assertNull(shellDto.getNewExecutable());
+    Assertions.assertNull(shellDto.getNewArguments());
+
+    json =
+        "{"
+            + "\"@type\":\"shell\","
+            + 
"\"newScripts\":[\"/path/to/script1.sh\",\"/path/to/script2.sh\"]"
+            + "}";
+
+    dto = JsonUtils.objectMapper().readValue(json, TemplateUpdateDTO.class);
+    Assertions.assertInstanceOf(ShellTemplateUpdateDTO.class, dto);
+    shellDto = (ShellTemplateUpdateDTO) dto;
+    Assertions.assertNull(shellDto.getNewEnvironments());
+    Assertions.assertNull(shellDto.getNewCustomFields());
+    Assertions.assertEquals(
+        ImmutableList.of("/path/to/script1.sh", "/path/to/script2.sh"), 
shellDto.getNewScripts());
+
+    json = "{" + "\"@type\":\"shell\"" + "}";
+
+    dto = JsonUtils.objectMapper().readValue(json, TemplateUpdateDTO.class);
+    Assertions.assertInstanceOf(ShellTemplateUpdateDTO.class, dto);
+    shellDto = (ShellTemplateUpdateDTO) dto;
+    Assertions.assertNull(shellDto.getNewScripts());
+  }
+
+  @Test
+  public void testDeserializeSparkTemplateUpdate() throws 
JsonProcessingException {
+    String json =
+        "{"
+            + "\"@type\":\"spark\","
+            + "\"newExecutable\":\"spark-submit\","
+            + "\"newArguments\":[\"--class\",\"org.example.Main\"],"
+            + "\"newEnvironments\":{\"SPARK_ENV\":\"prod\"},"
+            + "\"newCustomFields\":{\"customKey\":\"customValue\"},"
+            + "\"newClassName\":\"org.example.Main\","
+            + "\"newJars\":[\"/path/to/jar1.jar\",\"/path/to/jar2.jar\"],"
+            + "\"newFiles\":[\"/path/to/file1.txt\",\"/path/to/file2.txt\"],"
+            + 
"\"newArchives\":[\"/path/to/archive1.zip\",\"/path/to/archive2.zip\"],"
+            + "\"newConfigs\":{\"spark.executor.memory\":\"4g\"}"
+            + "}";
+
+    TemplateUpdateDTO dto = JsonUtils.objectMapper().readValue(json, 
TemplateUpdateDTO.class);
+    Assertions.assertInstanceOf(SparkTemplateUpdateDTO.class, dto);
+    SparkTemplateUpdateDTO sparkDto = (SparkTemplateUpdateDTO) dto;
+    Assertions.assertEquals("spark-submit", sparkDto.getNewExecutable());
+    Assertions.assertEquals(
+        ImmutableList.of("--class", "org.example.Main"), 
sparkDto.getNewArguments());
+    Assertions.assertEquals(ImmutableMap.of("SPARK_ENV", "prod"), 
sparkDto.getNewEnvironments());
+    Assertions.assertEquals(
+        ImmutableMap.of("customKey", "customValue"), 
sparkDto.getNewCustomFields());
+    Assertions.assertEquals("org.example.Main", sparkDto.getNewClassName());
+    Assertions.assertEquals(
+        ImmutableList.of("/path/to/jar1.jar", "/path/to/jar2.jar"), 
sparkDto.getNewJars());
+    Assertions.assertEquals(
+        ImmutableList.of("/path/to/file1.txt", "/path/to/file2.txt"), 
sparkDto.getNewFiles());
+    Assertions.assertEquals(
+        ImmutableList.of("/path/to/archive1.zip", "/path/to/archive2.zip"),
+        sparkDto.getNewArchives());
+    Assertions.assertEquals(
+        ImmutableMap.of("spark.executor.memory", "4g"), 
sparkDto.getNewConfigs());
+
+    json =
+        "{"
+            + "\"@type\":\"spark\","
+            + "\"newEnvironments\":{\"SPARK_ENV\":\"prod\"},"
+            + "\"newCustomFields\":{\"customKey\":\"customValue\"},"
+            + "\"newClassName\":\"org.example.Main\","
+            + "\"newJars\":[\"/path/to/jar1.jar\",\"/path/to/jar2.jar\"],"
+            + "\"newFiles\":[\"/path/to/file1.txt\",\"/path/to/file2.txt\"],"
+            + 
"\"newArchives\":[\"/path/to/archive1.zip\",\"/path/to/archive2.zip\"],"
+            + "\"newConfigs\":{\"spark.executor.memory\":\"4g\"}"
+            + "}";
+
+    dto = JsonUtils.objectMapper().readValue(json, TemplateUpdateDTO.class);
+    Assertions.assertInstanceOf(SparkTemplateUpdateDTO.class, dto);
+    sparkDto = (SparkTemplateUpdateDTO) dto;
+    Assertions.assertNull(sparkDto.getNewExecutable());
+    Assertions.assertNull(sparkDto.getNewArguments());
+    Assertions.assertEquals(ImmutableMap.of("SPARK_ENV", "prod"), 
sparkDto.getNewEnvironments());
+    Assertions.assertEquals(
+        ImmutableMap.of("customKey", "customValue"), 
sparkDto.getNewCustomFields());
+    Assertions.assertEquals("org.example.Main", sparkDto.getNewClassName());
+    Assertions.assertEquals(
+        ImmutableList.of("/path/to/jar1.jar", "/path/to/jar2.jar"), 
sparkDto.getNewJars());
+    Assertions.assertEquals(
+        ImmutableList.of("/path/to/file1.txt", "/path/to/file2.txt"), 
sparkDto.getNewFiles());
+    Assertions.assertEquals(
+        ImmutableList.of("/path/to/archive1.zip", "/path/to/archive2.zip"),
+        sparkDto.getNewArchives());
+    Assertions.assertEquals(
+        ImmutableMap.of("spark.executor.memory", "4g"), 
sparkDto.getNewConfigs());
+
+    json =
+        "{"
+            + "\"@type\":\"spark\","
+            + "\"newClassName\":\"org.example.Main\","
+            + "\"newJars\":[\"/path/to/jar1.jar\",\"/path/to/jar2.jar\"],"
+            + "\"newFiles\":[\"/path/to/file1.txt\",\"/path/to/file2.txt\"],"
+            + 
"\"newArchives\":[\"/path/to/archive1.zip\",\"/path/to/archive2.zip\"],"
+            + "\"newConfigs\":{\"spark.executor.memory\":\"4g\"}"
+            + "}";
+
+    dto = JsonUtils.objectMapper().readValue(json, TemplateUpdateDTO.class);
+    Assertions.assertInstanceOf(SparkTemplateUpdateDTO.class, dto);
+    sparkDto = (SparkTemplateUpdateDTO) dto;
+    Assertions.assertNull(sparkDto.getNewExecutable());
+    Assertions.assertNull(sparkDto.getNewArguments());
+    Assertions.assertNull(sparkDto.getNewEnvironments());
+    Assertions.assertNull(sparkDto.getNewCustomFields());
+
+    json = "{" + "\"@type\":\"spark\"," + 
"\"newConfigs\":{\"spark.executor.memory\":\"4g\"}" + "}";
+
+    dto = JsonUtils.objectMapper().readValue(json, TemplateUpdateDTO.class);
+    Assertions.assertInstanceOf(SparkTemplateUpdateDTO.class, dto);
+    sparkDto = (SparkTemplateUpdateDTO) dto;
+    Assertions.assertNull(sparkDto.getNewClassName());
+    Assertions.assertNull(sparkDto.getNewJars());
+    Assertions.assertNull(sparkDto.getNewFiles());
+    Assertions.assertNull(sparkDto.getNewArchives());
+    Assertions.assertEquals(
+        ImmutableMap.of("spark.executor.memory", "4g"), 
sparkDto.getNewConfigs());
+
+    json = "{" + "\"@type\":\"spark\"" + "}";
+
+    dto = JsonUtils.objectMapper().readValue(json, TemplateUpdateDTO.class);
+    Assertions.assertInstanceOf(SparkTemplateUpdateDTO.class, dto);
+    sparkDto = (SparkTemplateUpdateDTO) dto;
+    Assertions.assertNull(sparkDto.getNewConfigs());
+    Assertions.assertNull(sparkDto.getNewClassName());
+    Assertions.assertNull(sparkDto.getNewJars());
+    Assertions.assertNull(sparkDto.getNewFiles());
+    Assertions.assertNull(sparkDto.getNewArchives());
+    Assertions.assertNull(sparkDto.getNewExecutable());
+    Assertions.assertNull(sparkDto.getNewArguments());
+    Assertions.assertNull(sparkDto.getNewEnvironments());
+    Assertions.assertNull(sparkDto.getNewCustomFields());
+  }
+}
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestJobTemplateUpdatesRequest.java
 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestJobTemplateUpdatesRequest.java
new file mode 100644
index 0000000000..7a7a3b82df
--- /dev/null
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestJobTemplateUpdatesRequest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.dto.requests;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableList;
+import org.apache.gravitino.dto.job.ShellTemplateUpdateDTO;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestJobTemplateUpdatesRequest {
+
+  @Test
+  public void testSerDeRequest() throws JsonProcessingException {
+    JobTemplateUpdateRequest renameReq =
+        new 
JobTemplateUpdateRequest.RenameJobTemplateRequest("new_template_name");
+    JobTemplateUpdateRequest updateCommentReq =
+        new JobTemplateUpdateRequest.UpdateJobTemplateCommentRequest("Updated 
comment");
+    JobTemplateUpdateRequest updateContentReq =
+        new JobTemplateUpdateRequest.UpdateJobTemplateContentRequest(
+            
ShellTemplateUpdateDTO.builder().withNewExecutable("/bin/bash").build());
+
+    JobTemplateUpdatesRequest request =
+        new JobTemplateUpdatesRequest(
+            ImmutableList.of(renameReq, updateCommentReq, updateContentReq));
+
+    String json = JsonUtils.objectMapper().writeValueAsString(request);
+    JobTemplateUpdatesRequest deserializedRequest =
+        JsonUtils.objectMapper().readValue(json, 
JobTemplateUpdatesRequest.class);
+
+    Assertions.assertEquals(request, deserializedRequest);
+    
Assertions.assertTrue(deserializedRequest.getUpdates().contains(renameReq));
+    
Assertions.assertTrue(deserializedRequest.getUpdates().contains(updateCommentReq));
+    
Assertions.assertTrue(deserializedRequest.getUpdates().contains(updateContentReq));
+  }
+}
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java 
b/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java
index 54ea3c1365..ca5bc551e6 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java
@@ -33,6 +33,7 @@ import javax.ws.rs.DELETE;
 import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
@@ -46,6 +47,8 @@ import org.apache.gravitino.dto.job.ShellJobTemplateDTO;
 import org.apache.gravitino.dto.job.SparkJobTemplateDTO;
 import org.apache.gravitino.dto.requests.JobRunRequest;
 import org.apache.gravitino.dto.requests.JobTemplateRegisterRequest;
+import org.apache.gravitino.dto.requests.JobTemplateUpdateRequest;
+import org.apache.gravitino.dto.requests.JobTemplateUpdatesRequest;
 import org.apache.gravitino.dto.responses.BaseResponse;
 import org.apache.gravitino.dto.responses.DropResponse;
 import org.apache.gravitino.dto.responses.JobListResponse;
@@ -55,6 +58,7 @@ import org.apache.gravitino.dto.responses.JobTemplateResponse;
 import org.apache.gravitino.dto.responses.NameListResponse;
 import org.apache.gravitino.dto.util.DTOConverters;
 import org.apache.gravitino.job.JobOperationDispatcher;
+import org.apache.gravitino.job.JobTemplateChange;
 import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.JobEntity;
 import org.apache.gravitino.meta.JobTemplateEntity;
@@ -202,6 +206,40 @@ public class JobOperations {
     }
   }
 
+  @PUT
+  @Path("templates/{name}")
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "alter-job-template." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "alter-job-template", absolute = true)
+  public Response alterJobTemplate(
+      @PathParam("metalake") String metalake,
+      @PathParam("name") String jobTemplateName,
+      JobTemplateUpdatesRequest request) {
+    LOG.info(
+        "Received request to alter job template: {} in metalake: {}", 
jobTemplateName, metalake);
+
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () -> {
+            request.validate();
+            JobTemplateChange[] changes =
+                request.getUpdates().stream()
+                    .map(JobTemplateUpdateRequest::jobTemplateChange)
+                    .toArray(JobTemplateChange[]::new);
+
+            JobTemplateEntity updatedEntity =
+                jobOperationDispatcher.alterJobTemplate(metalake, 
jobTemplateName, changes);
+            Response response = Utils.ok(new 
JobTemplateResponse(toDTO(updatedEntity)));
+            LOG.info("Job template {} in metalake {} is altered", 
jobTemplateName, metalake);
+            return response;
+          });
+    } catch (Exception e) {
+      return ExceptionHandlers.handleJobTemplateException(
+          OperationType.ALTER, jobTemplateName, metalake, e);
+    }
+  }
+
   @GET
   @Path("runs")
   @Produces("application/vnd.gravitino.v1+json")
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestJobOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestJobOperations.java
index 56f4be73a3..452608c151 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestJobOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestJobOperations.java
@@ -39,8 +39,11 @@ import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.dto.job.JobTemplateDTO;
+import org.apache.gravitino.dto.job.ShellTemplateUpdateDTO;
 import org.apache.gravitino.dto.requests.JobRunRequest;
 import org.apache.gravitino.dto.requests.JobTemplateRegisterRequest;
+import org.apache.gravitino.dto.requests.JobTemplateUpdateRequest;
+import org.apache.gravitino.dto.requests.JobTemplateUpdatesRequest;
 import org.apache.gravitino.dto.responses.BaseResponse;
 import org.apache.gravitino.dto.responses.DropResponse;
 import org.apache.gravitino.dto.responses.ErrorConstants;
@@ -58,6 +61,7 @@ import 
org.apache.gravitino.exceptions.NoSuchJobTemplateException;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 import org.apache.gravitino.job.JobHandle;
 import org.apache.gravitino.job.JobOperationDispatcher;
+import org.apache.gravitino.job.JobTemplateChange;
 import org.apache.gravitino.job.ShellJobTemplate;
 import org.apache.gravitino.job.SparkJobTemplate;
 import org.apache.gravitino.meta.AuditInfo;
@@ -498,6 +502,131 @@ public class TestJobOperations extends JerseyTest {
     Assertions.assertEquals(InUseException.class.getSimpleName(), 
errorResp4.getType());
   }
 
+  @Test
+  public void testAlterJobTemplate() {
+    String templateName = "shell_template_1";
+    JobTemplateEntity template = newShellJobTemplateEntity(templateName, 
"Updated comment");
+    JobTemplateUpdateRequest renameReq =
+        new JobTemplateUpdateRequest.RenameJobTemplateRequest(templateName);
+    JobTemplateUpdateRequest updateCommentReq =
+        new JobTemplateUpdateRequest.UpdateJobTemplateCommentRequest("Updated 
comment");
+    JobTemplateUpdateRequest updateContentReq =
+        new JobTemplateUpdateRequest.UpdateJobTemplateContentRequest(
+            ShellTemplateUpdateDTO.builder().build());
+    JobTemplateUpdatesRequest req =
+        new JobTemplateUpdatesRequest(
+            Lists.newArrayList(renameReq, updateCommentReq, updateContentReq));
+    JobTemplateChange[] changes =
+        req.getUpdates().stream()
+            .map(JobTemplateUpdateRequest::jobTemplateChange)
+            .toArray(JobTemplateChange[]::new);
+
+    when(jobOperationDispatcher.alterJobTemplate(metalake, templateName, 
changes))
+        .thenReturn(template);
+
+    Response resp =
+        target(jobTemplatePath())
+            .path(templateName)
+            .request(APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity(req, APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(APPLICATION_JSON_TYPE, resp.getMediaType());
+
+    JobTemplateResponse jobTemplateResp = 
resp.readEntity(JobTemplateResponse.class);
+    Assertions.assertEquals(0, jobTemplateResp.getCode());
+    Assertions.assertEquals(JobOperations.toDTO(template), 
jobTemplateResp.getJobTemplate());
+
+    // Test throw NoSuchMetalakeException
+    doThrow(new NoSuchMetalakeException("mock error"))
+        .when(jobOperationDispatcher)
+        .alterJobTemplate(any(), any(), any());
+
+    Response resp2 =
+        target(jobTemplatePath())
+            .path(templateName)
+            .request(APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity(req, APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
resp2.getStatus());
+
+    ErrorResponse errorResp = resp2.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResp.getCode());
+    Assertions.assertEquals(NoSuchMetalakeException.class.getSimpleName(), 
errorResp.getType());
+
+    // Test throw MetalakeNotInUseException
+    doThrow(new MetalakeNotInUseException("mock error"))
+        .when(jobOperationDispatcher)
+        .alterJobTemplate(any(), any(), any());
+
+    Response resp3 =
+        target(jobTemplatePath())
+            .path(templateName)
+            .request(APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity(req, APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), 
resp3.getStatus());
+
+    ErrorResponse errorResp2 = resp3.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_IN_USE_CODE, 
errorResp2.getCode());
+    Assertions.assertEquals(MetalakeNotInUseException.class.getSimpleName(), 
errorResp2.getType());
+
+    // Test throw IllegalArgumentException
+    doThrow(new IllegalArgumentException("mock error"))
+        .when(jobOperationDispatcher)
+        .alterJobTemplate(any(), any(), any());
+
+    Response resp4 =
+        target(jobTemplatePath())
+            .path(templateName)
+            .request(APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity(req, APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
resp4.getStatus());
+    ErrorResponse errorResp3 = resp4.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, 
errorResp3.getCode());
+    Assertions.assertEquals(IllegalArgumentException.class.getSimpleName(), 
errorResp3.getType());
+
+    // Test throw NoSuchJobTemplateException
+    doThrow(new NoSuchJobTemplateException("mock error"))
+        .when(jobOperationDispatcher)
+        .alterJobTemplate(any(), any(), any());
+
+    Response resp5 =
+        target(jobTemplatePath())
+            .path(templateName)
+            .request(APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity(req, APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
resp5.getStatus());
+    ErrorResponse errorResp4 = resp5.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, 
errorResp4.getCode());
+    Assertions.assertEquals(NoSuchJobTemplateException.class.getSimpleName(), 
errorResp4.getType());
+
+    // Test throw RuntimeException
+    doThrow(new RuntimeException("mock error"))
+        .when(jobOperationDispatcher)
+        .alterJobTemplate(any(), any(), any());
+
+    Response resp6 =
+        target(jobTemplatePath())
+            .path(templateName)
+            .request(APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity(req, APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp6.getStatus());
+    ErrorResponse errorResp5 = resp6.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp5.getCode());
+    Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp5.getType());
+  }
+
   @Test
   public void testListJobs() {
     String templateName = "shell_template_1";


Reply via email to