Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,15 @@ public ResponseEntity<List<ProcessExecutionStep>> getStepsInfos(@Parameter(descr
return monitorService.getStepsInfos(executionId).map(list -> ResponseEntity.ok().body(list))
.orElseGet(() -> ResponseEntity.notFound().build());
}

@DeleteMapping("/executions/{executionId}")
@Operation(summary = "Delete an execution")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Execution was deleted"),
@ApiResponse(responseCode = "404", description = "Execution was not found")})
public ResponseEntity<Void> deleteExecution(@PathVariable UUID executionId) {
return monitorService.deleteExecution(executionId) ?
ResponseEntity.ok().build() :
ResponseEntity.notFound().build();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "processExecution")
@Table(name = "process_execution")
public class ProcessExecutionEntity {

@Id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "processExecutionStep")
@Table(name = "process_execution_step")
public class ProcessExecutionStepEntity {

@Id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/
package org.gridsuite.monitor.server.services;

import lombok.RequiredArgsConstructor;
import org.gridsuite.monitor.commons.ProcessConfig;
import org.gridsuite.monitor.commons.ProcessExecutionStep;
import org.gridsuite.monitor.commons.ProcessStatus;
Expand All @@ -23,6 +22,7 @@
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
Expand All @@ -31,14 +31,23 @@
* @author Antoine Bouhours <antoine.bouhours at rte-france.com>
*/
@Service
@RequiredArgsConstructor
public class MonitorService {

private final ProcessExecutionRepository executionRepository;
private final NotificationService notificationService;
private final ReportService reportService;
private final ResultService resultService;

public MonitorService(ProcessExecutionRepository executionRepository,
NotificationService notificationService,
ReportService reportService,
ResultService resultService) {
this.executionRepository = executionRepository;
this.notificationService = notificationService;
this.reportService = reportService;
this.resultService = resultService;
}

@Transactional
public UUID executeProcess(UUID caseUuid, String userId, ProcessConfig processConfig) {
ProcessExecutionEntity execution = ProcessExecutionEntity.builder()
Expand Down Expand Up @@ -180,4 +189,29 @@ public Optional<List<ProcessExecutionStep>> getStepsInfos(UUID executionId) {
return Optional.empty();
}
}

@Transactional
public boolean deleteExecution(UUID executionId) {
List<ResultInfos> resultIds = new ArrayList<>();
List<UUID> reportIds = new ArrayList<>();

Optional<ProcessExecutionEntity> executionEntity = executionRepository.findById(executionId);
if (executionEntity.isPresent()) {
Optional.ofNullable(executionEntity.get().getSteps()).orElse(List.of()).forEach(step -> {
if (step.getResultId() != null && step.getResultType() != null) {
Comment on lines +200 to +201
Copy link

@thangqp thangqp Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you choose to rely on Optional to check for null values here, you must ensure that the same checks are applied consistently throughout the codebase.

In fact, Hibernate always initializes a collection when loading a relationship of a collection type. However, to be extra safe, we can explicitly initialize an empty ArrayList in the parent entity, i.e. ProcessExecutionEntity

resultIds.add(new ResultInfos(step.getResultId(), step.getResultType()));
}
if (step.getReportId() != null) {
reportIds.add(step.getReportId());
}
});
resultIds.forEach(resultService::deleteResult);
reportIds.forEach(reportService::deleteReport);

executionRepository.deleteById(executionId);

return true;
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,12 @@ public ReportPage getReport(UUID reportId) {

return restTemplate.exchange(this.getReportServerURI() + path, HttpMethod.GET, new HttpEntity<>(headers), new ParameterizedTypeReference<ReportPage>() { }).getBody();
}

public void deleteReport(UUID reportId) {
var path = UriComponentsBuilder.fromPath("{id}")
.buildAndExpand(reportId)
.toUriString();

restTemplate.delete(this.getReportServerURI() + path);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public interface ResultProvider {
ResultType getType();

String getResult(UUID resultId);

void deleteResult(UUID resultId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,19 @@ public ResultService(List<ResultProvider> resultProviders) {
));
}

public String getResult(ResultInfos resultInfos) {
ResultProvider provider = providers.get(resultInfos.resultType());
if (provider != null) {
return provider.getResult(resultInfos.resultUUID());
} else {
throw new IllegalArgumentException("Unsupported result type: " + resultInfos.resultType());
public ResultProvider getProvider(ResultType resultType) {
ResultProvider provider = providers.get(resultType);
if (provider == null) {
throw new IllegalArgumentException("Unsupported result type: " + resultType);
}
return provider;
}

public String getResult(ResultInfos resultInfos) {
return getProvider(resultInfos.resultType()).getResult(resultInfos.resultUUID());
}

public void deleteResult(ResultInfos resultInfos) {
getProvider(resultInfos.resultType()).deleteResult(resultInfos.resultUUID());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ public ResultType getType() {
public String getResult(UUID resultId) {
return securityAnalysisService.getResult(resultId);
}

@Override
public void deleteResult(UUID resultId) {
securityAnalysisService.deleteResult(resultId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.List;
import java.util.UUID;

/**
Expand Down Expand Up @@ -52,4 +53,15 @@ public String getResult(UUID resultUuid) {

return restTemplate.exchange(getSecurityAnalysisServerBaseUri() + path, HttpMethod.GET, null, String.class).getBody();
}

public void deleteResult(UUID resultUuid) {
LOGGER.info("Deleting result {}", resultUuid);

var path = UriComponentsBuilder.fromPath("/results")
.queryParam("resultsUuids", List.of(resultUuid))
.build()
.toUriString();

restTemplate.delete(getSecurityAnalysisServerBaseUri() + path);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
Expand Down Expand Up @@ -183,4 +184,28 @@ void getStepsInfosShouldReturn404WhenExecutionNotFound() throws Exception {

verify(monitorService).getStepsInfos(executionId);
}

@Test
void deleteExecutionShouldReturnTrue() throws Exception {
UUID executionId = UUID.randomUUID();
when(monitorService.deleteExecution(executionId))
.thenReturn(Boolean.TRUE);

mockMvc.perform(delete("/v1/executions/{executionId}", executionId))
.andExpect(status().isOk());

verify(monitorService).deleteExecution(executionId);
}

@Test
void deleteExecutionShouldReturnFalse() throws Exception {
UUID executionId = UUID.randomUUID();
when(monitorService.deleteExecution(executionId))
.thenReturn(Boolean.FALSE);

mockMvc.perform(delete("/v1/executions/{executionId}", executionId))
.andExpect(status().isNotFound());

verify(monitorService).deleteExecution(executionId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -497,4 +497,58 @@ void getStepsInfosShouldReturnEmptyListWhenNoSteps() {
assertThat(result).isPresent();
assertThat(result.get()).isEmpty();
}

@Test
void deleteExecutionShouldDeleteResultsAndReports() {
UUID reportId1 = UUID.randomUUID();
UUID resultId1 = UUID.randomUUID();
UUID reportId2 = UUID.randomUUID();
UUID resultId2 = UUID.randomUUID();
ProcessExecutionStepEntity step0 = ProcessExecutionStepEntity.builder()
.id(UUID.randomUUID())
.stepOrder(0)
.reportId(reportId1)
.resultId(resultId1)
.build();
ProcessExecutionStepEntity step1 = ProcessExecutionStepEntity.builder()
.id(UUID.randomUUID())
.stepOrder(1)
.reportId(reportId2)
.resultId(resultId2)
.resultType(ResultType.SECURITY_ANALYSIS)
.build();
ProcessExecutionEntity execution = ProcessExecutionEntity.builder()
.id(executionId)
.steps(List.of(step0, step1))
.build();

when(executionRepository.findById(executionId)).thenReturn(Optional.of(execution));
doNothing().when(executionRepository).deleteById(executionId);

doNothing().when(reportService).deleteReport(reportId1);
doNothing().when(reportService).deleteReport(reportId2);
doNothing().when(resultService).deleteResult(any(ResultInfos.class));

boolean done = monitorService.deleteExecution(executionId);
assertThat(done).isTrue();

verify(executionRepository).findById(executionId);
verify(executionRepository).deleteById(executionId);
verify(reportService).deleteReport(reportId1);
verify(reportService).deleteReport(reportId2);
verify(resultService, times(1)).deleteResult(any(ResultInfos.class));
}

@Test
void deleteExecutionShouldReturnFalseWhenExecutionNotFound() {
when(executionRepository.findById(executionId)).thenReturn(Optional.empty());

boolean done = monitorService.deleteExecution(executionId);
assertThat(done).isFalse();

verify(executionRepository).findById(executionId);
verifyNoInteractions(reportService);
verifyNoInteractions(resultService);
verify(executionRepository, never()).deleteById(executionId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
Expand Down Expand Up @@ -71,4 +72,26 @@ void getReportFailed() {

assertThatThrownBy(() -> reportService.getReport(reportId)).isInstanceOf(RestClientException.class);
}

@Test
void deleteReport() {
UUID reportId = UUID.randomUUID();

server.expect(MockRestRequestMatchers.method(HttpMethod.DELETE))
.andExpect(MockRestRequestMatchers.requestTo("http://report-server/v1/reports/" + reportId))
.andRespond(MockRestResponseCreators.withSuccess());

assertThatNoException().isThrownBy(() -> reportService.deleteReport(reportId));
}

@Test
void deleteReportFailed() {
UUID reportId = UUID.randomUUID();

server.expect(MockRestRequestMatchers.method(HttpMethod.DELETE))
.andExpect(MockRestRequestMatchers.requestTo("http://report-server/v1/reports/" + reportId))
.andRespond(MockRestResponseCreators.withServerError());

assertThatThrownBy(() -> reportService.deleteReport(reportId)).isInstanceOf(RestClientException.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -58,4 +59,29 @@ void getResultShouldThrowExceptionWhenProviderNotFound() {
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Unsupported result type: " + ResultType.SECURITY_ANALYSIS);
}

@Test
void deleteResultShouldDeleteFromCorrectProvider() {
UUID resultId = UUID.randomUUID();
doNothing().when(resultProvider).deleteResult(resultId);
when(resultProvider.getType()).thenReturn(ResultType.SECURITY_ANALYSIS);
resultService = new ResultService(List.of(resultProvider));
ResultInfos resultInfos = new ResultInfos(resultId, ResultType.SECURITY_ANALYSIS);

resultService.deleteResult(resultInfos);

verify(resultProvider).deleteResult(resultId);
}

@Test
void deleteResultShouldThrowExceptionWhenProviderNotFound() {
UUID resultId = UUID.randomUUID();
ResultInfos resultInfos = new ResultInfos(resultId, ResultType.SECURITY_ANALYSIS);

ResultService emptyService = new ResultService(List.of());

assertThatThrownBy(() -> emptyService.deleteResult(resultInfos))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Unsupported result type: " + ResultType.SECURITY_ANALYSIS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,16 @@ void getResultShouldDelegateToSecurityAnalysisService() {
verify(securityAnalysisService).getResult(id);
verifyNoMoreInteractions(securityAnalysisService);
}

@Test
void deleteResultShouldDelegateToSecurityAnalysisService() {
UUID id = UUID.randomUUID();

doNothing().when(securityAnalysisService).deleteResult(id);

provider.deleteResult(id);

verify(securityAnalysisService).deleteResult(id);
verifyNoMoreInteractions(securityAnalysisService);
}
}
Loading