diff --git a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/AggregateDataFlowTaskExecutionQueryDao.java b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/AggregateDataFlowTaskExecutionQueryDao.java index a9ae8b0a8a..bf19fd7b64 100644 --- a/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/AggregateDataFlowTaskExecutionQueryDao.java +++ b/spring-cloud-dataflow-aggregate-task/src/main/java/org/springframework/cloud/dataflow/aggregate/task/impl/AggregateDataFlowTaskExecutionQueryDao.java @@ -22,12 +22,13 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; - +import java.util.stream.Collectors; import javax.sql.DataSource; import org.slf4j.Logger; @@ -89,6 +90,9 @@ public class AggregateDataFlowTaskExecutionQueryDao implements DataflowTaskExecu private static final String FIND_TASK_ARGUMENTS = "SELECT TASK_EXECUTION_ID, " + "TASK_PARAM from AGGREGATE_TASK_EXECUTION_PARAMS where TASK_EXECUTION_ID = :taskExecutionId and SCHEMA_TARGET = :schemaTarget"; + private static final String FIND_TASKS_ARGUMENTS = "SELECT TASK_EXECUTION_ID, " + + "TASK_PARAM from AGGREGATE_TASK_EXECUTION_PARAMS where TASK_EXECUTION_ID IN (:taskExecutionIds) and SCHEMA_TARGET = :schemaTarget"; + private static final String GET_EXECUTIONS = "SELECT " + SELECT_CLAUSE + " from AGGREGATE_TASK_EXECUTION"; @@ -217,7 +221,7 @@ public AggregateTaskExecution geTaskExecutionByExecutionId(String externalExecut return this.jdbcTemplate.queryForObject( GET_EXECUTION_BY_EXTERNAL_EXECUTION_ID, queryParameters, - new CompositeTaskExecutionRowMapper() + new CompositeTaskExecutionRowMapper(true) ); } catch (EmptyResultDataAccessException e) { return null; @@ -234,7 +238,7 @@ public AggregateTaskExecution getTaskExecution(long executionId, String schemaTa return this.jdbcTemplate.queryForObject( GET_EXECUTION_BY_ID, queryParameters, - new CompositeTaskExecutionRowMapper() + new CompositeTaskExecutionRowMapper(true) ); } catch (EmptyResultDataAccessException e) { return null; @@ -251,7 +255,7 @@ public List findChildTaskExecutions(long executionId, St return this.jdbcTemplate.query( GET_CHILD_EXECUTION_BY_ID, queryParameters, - new CompositeTaskExecutionRowMapper() + new CompositeTaskExecutionRowMapper(true) ); } catch (EmptyResultDataAccessException e) { return null; @@ -265,26 +269,44 @@ public List findChildTaskExecutions(Collection par .addValue("schemaTarget", "--spring.cloud.task.parent-schema-target=" + schemaTarget); try { - return this.jdbcTemplate.query( - GET_CHILD_EXECUTION_BY_IDS, - queryParameters, - new CompositeTaskExecutionRowMapper() + List result = this.jdbcTemplate.query( + GET_CHILD_EXECUTION_BY_IDS, + queryParameters, + new CompositeTaskExecutionRowMapper(false) ); + populateArguments(schemaTarget, result); + return result; } catch (EmptyResultDataAccessException e) { return null; } } + private void populateArguments(String schemaTarget, List result) { + List ids = result.stream().map(AggregateTaskExecution::getExecutionId).collect(Collectors.toList()); + Map> paramMap = getTaskArgumentsForTasks(ids, schemaTarget); + result.forEach(aggregateTaskExecution -> { + List params = paramMap.get(aggregateTaskExecution.getExecutionId()); + if(params != null) { + aggregateTaskExecution.setArguments(params); + } + }); + } + @Override public List findTaskExecutions(String taskName, boolean completed) { + List result; if (StringUtils.hasLength(taskName)) { final SqlParameterSource queryParameters = new MapSqlParameterSource() .addValue("taskName", taskName); String query = completed ? GET_EXECUTIONS_BY_NAME_COMPLETED : GET_EXECUTIONS_BY_NAME; - return this.jdbcTemplate.query(query, queryParameters, new CompositeTaskExecutionRowMapper()); + result = this.jdbcTemplate.query(query, queryParameters, new CompositeTaskExecutionRowMapper(false)); } else { - return this.jdbcTemplate.query(completed ? GET_EXECUTIONS_COMPLETED : GET_EXECUTIONS, Collections.emptyMap(), new CompositeTaskExecutionRowMapper()); + result = this.jdbcTemplate.query(completed ? GET_EXECUTIONS_COMPLETED : GET_EXECUTIONS, Collections.emptyMap(), new CompositeTaskExecutionRowMapper(false)); } + result.stream() + .collect(Collectors.groupingBy(AggregateTaskExecution::getSchemaTarget)) + .forEach(this::populateArguments); + return result; } @Override @@ -294,7 +316,11 @@ public List findTaskExecutionsBeforeEndTime(String taskN .addValue("endTime", endTime); String query; query = taskName.isEmpty() ? GET_EXECUTIONS_COMPLETED_BEFORE_END_TIME : GET_EXECUTION_BY_NAME_COMPLETED_BEFORE_END_TIME; - return this.jdbcTemplate.query(query, queryParameters, new CompositeTaskExecutionRowMapper()); + List result = this.jdbcTemplate.query(query, queryParameters, new CompositeTaskExecutionRowMapper(false)); + result.stream() + .collect(Collectors.groupingBy(AggregateTaskExecution::getSchemaTarget)) + .forEach(this::populateArguments); + return result; } @Override @@ -404,7 +430,11 @@ public List getLatestTaskExecutionsByTaskNames(String... try { final Map> paramMap = Collections .singletonMap("taskNames", taskNamesAsList); - return this.jdbcTemplate.query(LAST_TASK_EXECUTIONS_BY_TASK_NAMES, paramMap, new CompositeTaskExecutionRowMapper()); + List result = this.jdbcTemplate.query(LAST_TASK_EXECUTIONS_BY_TASK_NAMES, paramMap, new CompositeTaskExecutionRowMapper(false)); + result.stream() + .collect(Collectors.groupingBy(AggregateTaskExecution::getSchemaTarget)) + .forEach(this::populateArguments); + return result; } catch (EmptyResultDataAccessException e) { return Collections.emptyList(); } @@ -509,15 +539,19 @@ private Page queryForPageableResults( } String query = pagingQueryProvider.getPageQuery(pageable); List resultList = this.jdbcTemplate.query(query, - queryParameters, new CompositeTaskExecutionRowMapper()); + queryParameters, new CompositeTaskExecutionRowMapper(false)); + resultList.stream() + .collect(Collectors.groupingBy(AggregateTaskExecution::getSchemaTarget)) + .forEach(this::populateArguments); return new PageImpl<>(resultList, pageable, totalCount); } private class CompositeTaskExecutionRowMapper implements RowMapper { - - private CompositeTaskExecutionRowMapper() { - } + final boolean mapRow; + private CompositeTaskExecutionRowMapper(boolean mapRow) { + this.mapRow = mapRow; + } @Override public AggregateTaskExecution mapRow(ResultSet rs, int rowNum) throws SQLException { @@ -536,7 +570,7 @@ public AggregateTaskExecution mapRow(ResultSet rs, int rowNum) throws SQLExcepti rs.getTimestamp("START_TIME"), rs.getTimestamp("END_TIME"), rs.getString("EXIT_MESSAGE"), - getTaskArguments(id, schemaTarget), + mapRow ? getTaskArguments(id, schemaTarget) : Collections.emptyList(), rs.getString("ERROR_MESSAGE"), rs.getString("EXTERNAL_EXECUTION_ID"), parentExecutionId, @@ -554,11 +588,25 @@ private Integer getNullableExitCode(ResultSet rs) throws SQLException { private List getTaskArguments(long taskExecutionId, String schemaTarget) { final List params = new ArrayList<>(); RowCallbackHandler handler = rs -> params.add(rs.getString(2)); + MapSqlParameterSource parameterSource = new MapSqlParameterSource("taskExecutionId", taskExecutionId) + .addValue("schemaTarget", schemaTarget); this.jdbcTemplate.query( FIND_TASK_ARGUMENTS, - new MapSqlParameterSource("taskExecutionId", taskExecutionId) - .addValue("schemaTarget", schemaTarget), + parameterSource, handler); return params; } + private Map> getTaskArgumentsForTasks(Collection taskExecutionIds, String schemaTarget) { + if(taskExecutionIds.isEmpty()) { + return Collections.emptyMap(); + } else { + final Map> result = new HashMap<>(); + RowCallbackHandler handler = rs -> result.computeIfAbsent(rs.getLong(1), a -> new ArrayList<>()) + .add(rs.getString(2)); + MapSqlParameterSource parameterSource = new MapSqlParameterSource("taskExecutionIds", taskExecutionIds) + .addValue("schemaTarget", schemaTarget); + this.jdbcTemplate.query(FIND_TASKS_ARGUMENTS, parameterSource, handler); + return result; + } + } } diff --git a/spring-cloud-dataflow-classic-docs/src/test/java/org/springframework/cloud/dataflow/server/rest/documentation/ApiDocumentation.java b/spring-cloud-dataflow-classic-docs/src/test/java/org/springframework/cloud/dataflow/server/rest/documentation/ApiDocumentation.java index 57d1675631..e4f271e34b 100644 --- a/spring-cloud-dataflow-classic-docs/src/test/java/org/springframework/cloud/dataflow/server/rest/documentation/ApiDocumentation.java +++ b/spring-cloud-dataflow-classic-docs/src/test/java/org/springframework/cloud/dataflow/server/rest/documentation/ApiDocumentation.java @@ -40,6 +40,7 @@ * @author Gunnar Hillert * @author Christian Tzolov * @author Ilayaperumal Gopinathan + * @author Corneil du Plessis */ @SuppressWarnings("NewClassNamingConvention") public class ApiDocumentation extends BaseDocumentation { @@ -117,6 +118,7 @@ public void index() throws Exception { linkWithRel("tasks/executions/launch").description("Provides for launching a Task execution"), linkWithRel("tasks/executions/external").description("Returns Task execution by external id"), linkWithRel("tasks/executions/current").description("Provides the current count of running tasks"), + linkWithRel("tasks/thinexecutions").description("Returns thin Task executions"), linkWithRel("tasks/info/executions").description("Provides the task executions info"), linkWithRel("tasks/schedules").description("Provides schedule information of tasks"), linkWithRel("tasks/schedules/instances").description("Provides schedule information of a specific task "), diff --git a/spring-cloud-dataflow-classic-docs/src/test/java/org/springframework/cloud/dataflow/server/rest/documentation/TaskExecutionsDocumentation.java b/spring-cloud-dataflow-classic-docs/src/test/java/org/springframework/cloud/dataflow/server/rest/documentation/TaskExecutionsDocumentation.java index 5d0c23f961..66379d3b4a 100644 --- a/spring-cloud-dataflow-classic-docs/src/test/java/org/springframework/cloud/dataflow/server/rest/documentation/TaskExecutionsDocumentation.java +++ b/spring-cloud-dataflow-classic-docs/src/test/java/org/springframework/cloud/dataflow/server/rest/documentation/TaskExecutionsDocumentation.java @@ -263,6 +263,39 @@ public void listTaskExecutions() throws Exception { subsectionWithPath("page").description("Pagination properties")))); } + @Test + public void listTaskThinExecutions() throws Exception { + documentation.dontDocument(() -> this.mockMvc.perform( + post("/tasks/executions") + .param("name", "taskB") + .param("properties", "app.my-task.foo=bar,deployer.my-task.something-else=3") + .param("arguments", "--server.port=8080 --foo=bar") + ) + .andExpect(status().isCreated())); + + this.mockMvc.perform( + get("/tasks/thinexecutions") + .param("page", "1") + .param("size", "2")) + .andDo(print()) + .andExpect(status().isOk()).andDo(this.documentationHandler.document( + requestParameters( + parameterWithName("page") + .description("The zero-based page number (optional)"), + parameterWithName("size") + .description("The requested page size (optional)") + ), + responseFields( + subsectionWithPath("_embedded.taskExecutionThinResourceList") + .description("Contains a collection of thin Task Executions/"), + subsectionWithPath("_links.self").description("Link to the task execution resource"), + subsectionWithPath("_links.first").description("Link to the first page of task execution resources").optional(), + subsectionWithPath("_links.last").description("Link to the last page of task execution resources").optional(), + subsectionWithPath("_links.next").description("Link to the next page of task execution resources").optional(), + subsectionWithPath("_links.prev").description("Link to the previous page of task execution resources").optional(), + subsectionWithPath("page").description("Pagination properties")))); + } + @Test public void listTaskExecutionsByName() throws Exception { this.mockMvc.perform( diff --git a/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/TaskOperations.java b/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/TaskOperations.java index 1d87c3c6ab..c38cc83135 100644 --- a/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/TaskOperations.java +++ b/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/TaskOperations.java @@ -28,6 +28,7 @@ import org.springframework.cloud.dataflow.rest.resource.TaskAppStatusResource; import org.springframework.cloud.dataflow.rest.resource.TaskDefinitionResource; import org.springframework.cloud.dataflow.rest.resource.TaskExecutionResource; +import org.springframework.cloud.dataflow.rest.resource.TaskExecutionThinResource; import org.springframework.hateoas.PagedModel; /** @@ -37,6 +38,7 @@ * @author Michael Minella * @author Gunnar Hillert * @author David Turanski + * @author Corneil du Plessis */ public interface TaskOperations { @@ -108,6 +110,11 @@ public interface TaskOperations { */ PagedModel executionList(); + /** + * @return the list of thin task executions known to the system. + */ + PagedModel thinExecutionList(); + /** * List task executions known to the system filtered by task name. * diff --git a/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/TaskTemplate.java b/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/TaskTemplate.java index 71bcad460d..4400266ec4 100644 --- a/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/TaskTemplate.java +++ b/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/TaskTemplate.java @@ -16,13 +16,12 @@ package org.springframework.cloud.dataflow.rest.client; -import javax.naming.OperationNotSupportedException; - import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import javax.naming.OperationNotSupportedException; import org.springframework.cloud.dataflow.rest.client.support.VersionUtils; import org.springframework.cloud.dataflow.rest.resource.CurrentTaskExecutionsResource; @@ -31,12 +30,14 @@ import org.springframework.cloud.dataflow.rest.resource.TaskAppStatusResource; import org.springframework.cloud.dataflow.rest.resource.TaskDefinitionResource; import org.springframework.cloud.dataflow.rest.resource.TaskExecutionResource; +import org.springframework.cloud.dataflow.rest.resource.TaskExecutionThinResource; import org.springframework.cloud.dataflow.rest.resource.TaskExecutionsInfoResource; import org.springframework.cloud.dataflow.rest.resource.about.AboutResource; import org.springframework.cloud.dataflow.rest.util.DeploymentPropertiesUtils; import org.springframework.cloud.dataflow.schema.SchemaVersionTarget; import org.springframework.core.ParameterizedTypeReference; import org.springframework.hateoas.Link; +import org.springframework.hateoas.PagedModel; import org.springframework.hateoas.RepresentationModel; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; @@ -53,6 +54,7 @@ * @author Michael Minella * @author Gunnar Hillert * @author David Turanski + * @author Corneil du Plessis */ public class TaskTemplate implements TaskOperations { @@ -66,6 +68,8 @@ public class TaskTemplate implements TaskOperations { private static final String EXECUTIONS_RELATION = "tasks/executions"; + private static final String THIN_EXECUTIONS_RELATION = "tasks/thinexecutions"; + private static final String EXECUTIONS_CURRENT_RELATION = "tasks/executions/current"; private static final String EXECUTION_RELATION = "tasks/executions/execution"; @@ -90,6 +94,8 @@ public class TaskTemplate implements TaskOperations { private final Link executionsLink; + private final Link thinExecutionsLink; + private final Link executionLink; private final Link executionLaunchLink; @@ -114,6 +120,7 @@ public class TaskTemplate implements TaskOperations { Assert.notNull(restTemplate, "RestTemplate must not be null"); Assert.isTrue(resources.getLink("about").isPresent(), "Expected about relation"); Assert.isTrue(resources.getLink(EXECUTIONS_RELATION).isPresent(), "Executions relation is required"); + Assert.isTrue(resources.getLink(THIN_EXECUTIONS_RELATION).isPresent(), "Executions relation is required"); Assert.isTrue(resources.getLink(DEFINITIONS_RELATION).isPresent(), "Definitions relation is required"); Assert.isTrue(resources.getLink(DEFINITION_RELATION).isPresent(), "Definition relation is required"); Assert.isTrue(resources.getLink(EXECUTIONS_RELATION).isPresent(), "Executions relation is required"); @@ -133,7 +140,11 @@ public class TaskTemplate implements TaskOperations { if (VersionUtils.isDataFlowServerVersionGreaterThanOrEqualToRequiredVersion( VersionUtils.getThreePartVersion(dataFlowServerVersion), EXECUTIONS_CURRENT_RELATION_VERSION)) { + Assert.isTrue(resources.getLink(EXECUTIONS_CURRENT_RELATION).isPresent(), "Current Executions relation is required"); Assert.notNull(resources.getLink(EXECUTIONS_CURRENT_RELATION), "Executions current relation is required"); + this.executionsCurrentLink = resources.getLink(EXECUTIONS_CURRENT_RELATION).get(); + } else { + this.executionsCurrentLink = null; } this.restTemplate = restTemplate; @@ -141,6 +152,7 @@ public class TaskTemplate implements TaskOperations { this.definitionsLink = resources.getLink(DEFINITIONS_RELATION).get(); this.definitionLink = resources.getLink(DEFINITION_RELATION).get(); this.executionsLink = resources.getLink(EXECUTIONS_RELATION).get(); + this.thinExecutionsLink = resources.getLink(THIN_EXECUTIONS_RELATION).get(); this.executionLink = resources.getLink(EXECUTION_RELATION).get(); if(resources.getLink(EXECUTION_LAUNCH_RELATION).isPresent()) { this.executionLaunchLink = resources.getLink(EXECUTION_LAUNCH_RELATION).get(); @@ -148,7 +160,6 @@ public class TaskTemplate implements TaskOperations { this.executionLaunchLink = null; } this.executionByNameLink = resources.getLink(EXECUTION_RELATION_BY_NAME).get(); - this.executionsCurrentLink = resources.getLink(EXECUTIONS_CURRENT_RELATION).get(); if (resources.getLink(EXECUTIONS_INFO_RELATION).isPresent()) { this.executionsInfoLink = resources.getLink(EXECUTIONS_INFO_RELATION).get(); } @@ -256,6 +267,11 @@ public TaskExecutionResource.Page executionList() { return restTemplate.getForObject(executionsLink.getHref(), TaskExecutionResource.Page.class); } + @Override + public PagedModel thinExecutionList() { + return restTemplate.getForObject(thinExecutionsLink.getHref(), TaskExecutionThinResource.Page.class); + } + @Override public TaskExecutionResource.Page executionListByTaskName(String taskName) { return restTemplate.getForObject(executionByNameLink.expand(taskName).getHref(), diff --git a/spring-cloud-dataflow-rest-resource/src/main/java/org/springframework/cloud/dataflow/rest/resource/TaskExecutionThinResource.java b/spring-cloud-dataflow-rest-resource/src/main/java/org/springframework/cloud/dataflow/rest/resource/TaskExecutionThinResource.java new file mode 100644 index 0000000000..46946df8ea --- /dev/null +++ b/spring-cloud-dataflow-rest-resource/src/main/java/org/springframework/cloud/dataflow/rest/resource/TaskExecutionThinResource.java @@ -0,0 +1,183 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.cloud.dataflow.rest.resource; + +import java.util.Date; + +import org.springframework.cloud.dataflow.schema.AggregateTaskExecution; +import org.springframework.hateoas.PagedModel; +import org.springframework.hateoas.RepresentationModel; + +/** + * This resource is a match for AggregateTaskExecution and should satisfy UI paging. + * @author Corneil du Plessis + */ +public class TaskExecutionThinResource extends RepresentationModel { + /** + * The unique id associated with the task execution. + */ + private long executionId; + + /** + * The parent task execution id. + */ + private Long parentExecutionId; + + /** + * The recorded exit code for the task. + */ + private Integer exitCode; + + /** + * User defined name for the task. + */ + private String taskName; + + /** + * Time of when the task was started. + */ + private Date startTime; + + /** + * Timestamp of when the task was completed/terminated. + */ + private Date endTime; + + /** + * Message returned from the task or stacktrace. + */ + private String exitMessage; + + private String externalExecutionId; + + + private String errorMessage; + + /** + * @since 2.11.0 + */ + + private String schemaTarget; + + private String platformName; + + public TaskExecutionThinResource() { + } + + public TaskExecutionThinResource(AggregateTaskExecution aggregateTaskExecution) { + this.executionId = aggregateTaskExecution.getExecutionId(); + this.schemaTarget = aggregateTaskExecution.getSchemaTarget(); + this.taskName = aggregateTaskExecution.getTaskName(); + this.platformName = aggregateTaskExecution.getPlatformName(); + this.externalExecutionId = aggregateTaskExecution.getExternalExecutionId(); + this.parentExecutionId =aggregateTaskExecution.getParentExecutionId(); + this.startTime = aggregateTaskExecution.getStartTime(); + this.endTime = aggregateTaskExecution.getEndTime(); + this.exitCode = aggregateTaskExecution.getExitCode(); + this.exitMessage = aggregateTaskExecution.getExitMessage(); + this.errorMessage = aggregateTaskExecution.getErrorMessage(); + } + + public long getExecutionId() { + return executionId; + } + + public void setExecutionId(long executionId) { + this.executionId = executionId; + } + + public Long getParentExecutionId() { + return parentExecutionId; + } + + public void setParentExecutionId(Long parentExecutionId) { + this.parentExecutionId = parentExecutionId; + } + + public Integer getExitCode() { + return exitCode; + } + + public void setExitCode(Integer exitCode) { + this.exitCode = exitCode; + } + + public String getTaskName() { + return taskName; + } + + public void setTaskName(String taskName) { + this.taskName = taskName; + } + + public Date getStartTime() { + return startTime; + } + + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + public Date getEndTime() { + return endTime; + } + + public void setEndTime(Date endTime) { + this.endTime = endTime; + } + + public String getExitMessage() { + return exitMessage; + } + + public void setExitMessage(String exitMessage) { + this.exitMessage = exitMessage; + } + + public String getExternalExecutionId() { + return externalExecutionId; + } + + public void setExternalExecutionId(String externalExecutionId) { + this.externalExecutionId = externalExecutionId; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getSchemaTarget() { + return schemaTarget; + } + + public void setSchemaTarget(String schemaTarget) { + this.schemaTarget = schemaTarget; + } + + public String getPlatformName() { + return platformName; + } + + public void setPlatformName(String platformName) { + this.platformName = platformName; + } + public static class Page extends PagedModel { + } +} diff --git a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataFlowControllerAutoConfiguration.java b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataFlowControllerAutoConfiguration.java index c1e99eb963..8f6e467bc4 100644 --- a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataFlowControllerAutoConfiguration.java +++ b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataFlowControllerAutoConfiguration.java @@ -40,6 +40,8 @@ import org.springframework.cloud.common.security.core.support.OAuth2AccessTokenProvidingClientHttpRequestInterceptor; import org.springframework.cloud.common.security.core.support.OAuth2TokenUtilsService; import org.springframework.cloud.common.security.support.SecurityStateBean; +import org.springframework.cloud.dataflow.aggregate.task.AggregateExecutionSupport; +import org.springframework.cloud.dataflow.aggregate.task.AggregateTaskExplorer; import org.springframework.cloud.dataflow.aggregate.task.TaskDefinitionReader; import org.springframework.cloud.dataflow.audit.repository.AuditRecordRepository; import org.springframework.cloud.dataflow.audit.service.AuditRecordService; @@ -85,6 +87,7 @@ import org.springframework.cloud.dataflow.server.controller.TaskCtrController; import org.springframework.cloud.dataflow.server.controller.TaskDefinitionController; import org.springframework.cloud.dataflow.server.controller.TaskExecutionController; +import org.springframework.cloud.dataflow.server.controller.TaskExecutionThinController; import org.springframework.cloud.dataflow.server.controller.TaskLogsController; import org.springframework.cloud.dataflow.server.controller.TaskPlatformController; import org.springframework.cloud.dataflow.server.controller.TaskSchedulerController; @@ -101,8 +104,6 @@ import org.springframework.cloud.dataflow.server.job.LauncherRepository; import org.springframework.cloud.dataflow.server.repository.StreamDefinitionRepository; import org.springframework.cloud.dataflow.server.repository.TaskDefinitionRepository; -import org.springframework.cloud.dataflow.aggregate.task.AggregateExecutionSupport; -import org.springframework.cloud.dataflow.aggregate.task.AggregateTaskExplorer; import org.springframework.cloud.dataflow.server.service.JobServiceContainer; import org.springframework.cloud.dataflow.server.service.LauncherService; import org.springframework.cloud.dataflow.server.service.SchedulerService; @@ -298,6 +299,11 @@ public TaskExecutionController taskExecutionController( ); } + @Bean + public TaskExecutionThinController taskExecutionThinController(AggregateTaskExplorer aggregateTaskExplorer) { + return new TaskExecutionThinController(aggregateTaskExplorer); + } + @Bean public TaskPlatformController taskLauncherController(LauncherService launcherService) { return new TaskPlatformController(launcherService); diff --git a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/controller/TaskExecutionThinController.java b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/controller/TaskExecutionThinController.java new file mode 100644 index 0000000000..f83aecde5a --- /dev/null +++ b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/controller/TaskExecutionThinController.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.cloud.dataflow.server.controller; + +import org.springframework.cloud.dataflow.aggregate.task.AggregateTaskExplorer; +import org.springframework.cloud.dataflow.rest.resource.TaskExecutionThinResource; +import org.springframework.cloud.dataflow.schema.AggregateTaskExecution; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.hateoas.PagedModel; +import org.springframework.hateoas.server.ExposesResourceFor; +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * This controller provides for retrieving a thin task execution resource that will satisfy UI paging with embedded links to more detail. + * @author Corneil du Plessis + */ +@RestController +@RequestMapping("/tasks/thinexecutions") +@ExposesResourceFor(TaskExecutionThinResource.class) +public class TaskExecutionThinController { + + private final AggregateTaskExplorer explorer; + private final TaskExecutionThinResourceAssembler resourceAssembler; + + public TaskExecutionThinController(AggregateTaskExplorer explorer) { + this.explorer = explorer; + this.resourceAssembler = new TaskExecutionThinResourceAssembler(); + } + + @GetMapping(produces = "application/json") + @ResponseStatus(HttpStatus.OK) + public PagedModel listTasks(Pageable pageable, PagedResourcesAssembler pagedAssembler) { + return pagedAssembler.toModel(explorer.findAll(pageable), resourceAssembler); + } + + static class TaskExecutionThinResourceAssembler extends RepresentationModelAssemblerSupport { + public TaskExecutionThinResourceAssembler() { + super(TaskExecutionThinController.class, TaskExecutionThinResource.class); + } + @Override + public TaskExecutionThinResource toModel(AggregateTaskExecution entity) { + TaskExecutionThinResource resource = new TaskExecutionThinResource(entity); + resource.add(linkTo(methodOn(TaskExecutionController.class).view(resource.getExecutionId(), resource.getSchemaTarget())).withSelfRel()); + resource.add(linkTo(methodOn(TaskDefinitionController.class).display(resource.getTaskName(), true)).withRel("tasks/definitions")); + return resource; + } + } +} diff --git a/spring-cloud-dataflow-server-core/src/main/resources/META-INF/dataflow-server-defaults.yml b/spring-cloud-dataflow-server-core/src/main/resources/META-INF/dataflow-server-defaults.yml index 2cfe787d68..63a9a3badb 100644 --- a/spring-cloud-dataflow-server-core/src/main/resources/META-INF/dataflow-server-defaults.yml +++ b/spring-cloud-dataflow-server-core/src/main/resources/META-INF/dataflow-server-defaults.yml @@ -227,6 +227,8 @@ spring: - DELETE /tasks/executions => hasRole('ROLE_DESTROY') - GET /tasks/info/* => hasRole('ROLE_VIEW') + - GET /tasks/thinexecutions => hasRole('ROLE_VIEW') + # Task Schedules - GET /tasks/schedules => hasRole('ROLE_VIEW') diff --git a/spring-cloud-dataflow-server-core/src/test/java/org/springframework/cloud/dataflow/server/configuration/JobDependencies.java b/spring-cloud-dataflow-server-core/src/test/java/org/springframework/cloud/dataflow/server/configuration/JobDependencies.java index 0854ca5390..8c033241a8 100644 --- a/spring-cloud-dataflow-server-core/src/test/java/org/springframework/cloud/dataflow/server/configuration/JobDependencies.java +++ b/spring-cloud-dataflow-server-core/src/test/java/org/springframework/cloud/dataflow/server/configuration/JobDependencies.java @@ -70,6 +70,7 @@ import org.springframework.cloud.dataflow.server.controller.RestControllerAdvice; import org.springframework.cloud.dataflow.server.controller.SchemaController; import org.springframework.cloud.dataflow.server.controller.TaskExecutionController; +import org.springframework.cloud.dataflow.server.controller.TaskExecutionThinController; import org.springframework.cloud.dataflow.server.controller.TaskLogsController; import org.springframework.cloud.dataflow.server.controller.TaskPlatformController; import org.springframework.cloud.dataflow.server.controller.TasksInfoController; @@ -239,6 +240,11 @@ public TaskExecutionController taskExecutionController( ); } + @Bean + public TaskExecutionThinController taskExecutionThinController(AggregateTaskExplorer aggregateTaskExplorer) { + return new TaskExecutionThinController(aggregateTaskExplorer); + } + @Bean public TasksInfoController taskExecutionsInfoController(TaskExecutionService taskExecutionService) { return new TasksInfoController(taskExecutionService); diff --git a/spring-cloud-dataflow-server-core/src/test/java/org/springframework/cloud/dataflow/server/controller/TaskExecutionControllerTests.java b/spring-cloud-dataflow-server-core/src/test/java/org/springframework/cloud/dataflow/server/controller/TaskExecutionControllerTests.java index 93e672b701..6b30ed741a 100644 --- a/spring-cloud-dataflow-server-core/src/test/java/org/springframework/cloud/dataflow/server/controller/TaskExecutionControllerTests.java +++ b/spring-cloud-dataflow-server-core/src/test/java/org/springframework/cloud/dataflow/server/controller/TaskExecutionControllerTests.java @@ -362,6 +362,16 @@ void getAllExecutions() throws Exception { .andExpect(jsonPath("$._embedded.taskExecutionResourceList", hasSize(4))); } + @Test + void getAllThinExecutions() throws Exception { + mockMvc.perform(get("/tasks/thinexecutions").accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.taskExecutionThinResourceList[*].executionId", containsInAnyOrder(4, 3, 2, 1))) + .andExpect(jsonPath("$._embedded.taskExecutionThinResourceList[*].parentExecutionId", containsInAnyOrder(null, null, null, 1))) + .andExpect(jsonPath("$._embedded.taskExecutionThinResourceList", hasSize(4))); + } + @Test void getCurrentExecutions() throws Exception { when(taskLauncher.getRunningTaskExecutionCount()).thenReturn(4);