diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java index 182ccf450b49..698b4db4e654 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java @@ -104,6 +104,39 @@ public static boolean sendSyncRequest( return false; } + public static Optional runSyncRequest( + RestTemplate restTemplate, + RequestCallback requestCallback, + ResponseExtractor responseExtractor, + String syncUrl, + int maxSyncAttempts) { + boolean networkErrorOccurred = true; + int syncAttemptsDone = 0; + ImportSummary responseSummary = null; + + while (networkErrorOccurred) { + networkErrorOccurred = false; + syncAttemptsDone++; + try { + responseSummary = + restTemplate.execute(syncUrl, HttpMethod.POST, requestCallback, responseExtractor); + } catch (HttpServerErrorException ex) { + log.error( + "Internal error happened during sync request: {}", ex.getResponseBodyAsString(), ex); + if (syncAttemptsDone <= maxSyncAttempts) { + networkErrorOccurred = true; + } else { + throw ex; + } + } catch (ResourceAccessException ex) { + log.error("Exception during sync request: {}", ex.getMessage(), ex); + throw ex; + } + } + + return Optional.ofNullable(responseSummary); + } + public static Optional runSyncRequest( RestTemplate restTemplate, RequestCallback requestCallback, diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/PersistablesFilter.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/PersistablesFilter.java index 6db6e8e36c9d..42a567c21577 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/PersistablesFilter.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/PersistablesFilter.java @@ -174,12 +174,27 @@ private void filter() { private void collectDeletables(Class type, List entities) { for (T entity : entities) { - if (isValid(entity)) { + if (isValid(entity) && !isAlreadyDeleted(entity)) { collectPersistable(type, entity); } } } + private boolean isAlreadyDeleted(TrackerDto entity) { + if (entity instanceof Event ev) { + org.hisp.dhis.program.Event existing = preheat.getEvent(ev.getEvent()); + return existing != null && existing.isDeleted(); + } else if (entity instanceof Enrollment en) { + org.hisp.dhis.program.Enrollment existing = preheat.getEnrollment(en.getEnrollment()); + return existing != null && existing.isDeleted(); + } else if (entity instanceof TrackedEntity te) { + org.hisp.dhis.trackedentity.TrackedEntity existing = + preheat.getTrackedEntity(te.getTrackedEntity()); + return existing != null && existing.isDeleted(); + } + return false; + } + private void collectPersistables( Class type, List> parents, List entities) { for (T entity : entities) { diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/ExistenceValidator.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/ExistenceValidator.java index 4e1a8ad44f1e..5e316e1db174 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/ExistenceValidator.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/ExistenceValidator.java @@ -50,10 +50,12 @@ public void validate( Event existingEvent = bundle.getPreheat().getEvent(event.getEvent()); - // If the event is soft-deleted no operation is allowed if (existingEvent != null && existingEvent.isDeleted()) { - reporter.addError(event, E1082, event.getEvent()); - return; + if (importStrategy.isDelete()) { + reporter.addWarning(event, E1082, event.getEvent()); + } else { + reporter.addError(event, E1082, event.getEvent()); + } } if (existingEvent != null && importStrategy.isCreate()) { diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java index e446918524a9..6dcb4f583427 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java @@ -33,10 +33,10 @@ import static org.hisp.dhis.dxf2.sync.SyncUtils.runSyncRequest; import static org.hisp.dhis.scheduling.JobProgress.FailurePolicy.SKIP_ITEM; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Date; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -44,6 +44,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hisp.dhis.common.UID; +import org.hisp.dhis.commons.jackson.config.JacksonObjectMapperConfig; +import org.hisp.dhis.dxf2.importsummary.ImportCount; import org.hisp.dhis.dxf2.importsummary.ImportStatus; import org.hisp.dhis.dxf2.importsummary.ImportSummary; import org.hisp.dhis.dxf2.metadata.sync.exception.MetadataSyncServiceException; @@ -65,8 +67,9 @@ import org.hisp.dhis.tracker.export.event.EventOperationParams; import org.hisp.dhis.tracker.export.event.EventService; import org.hisp.dhis.tracker.imports.TrackerImportStrategy; +import org.hisp.dhis.tracker.imports.report.ImportReport; +import org.hisp.dhis.tracker.imports.report.Stats; import org.hisp.dhis.webapi.controller.tracker.export.event.EventMapper; -import org.hisp.dhis.webmessage.WebMessageResponse; import org.mapstruct.factory.Mappers; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; @@ -82,6 +85,7 @@ public class SingleEventDataSynchronizationService extends TrackerDataSynchronizationWithPaging { private static final String PROCESS_NAME = "Single event programs data synchronization"; private static final EventMapper EVENT_MAPPER = Mappers.getMapper(EventMapper.class); + private static final ObjectMapper JSON_MAPPER = JacksonObjectMapperConfig.staticJsonMapper(); private final EventService eventService; private final SystemSettingsService systemSettingsService; @@ -292,16 +296,32 @@ private ImportSummary sendTrackerRequest( SystemSettings settings, String url) { RequestCallback requestCallback = createRequestCallback(events, instance); - - Optional response = - runSyncRequest( + String syncUrl = url + "&async=false"; + return runSyncRequest( restTemplate, requestCallback, - SyncEndpoint.TRACKER_IMPORT.getKlass(), - url, - settings.getSyncMaxAttempts()); + response -> + toImportSummary(JSON_MAPPER.readValue(response.getBody(), ImportReport.class)), + syncUrl, + settings.getSyncMaxAttempts()) + .orElse(null); + } - return response.map(ImportSummary.class::cast).orElse(null); + static ImportSummary toImportSummary(ImportReport report) { + ImportSummary summary = new ImportSummary(); + summary.setStatus( + switch (report.getStatus()) { + case OK -> ImportStatus.SUCCESS; + case WARNING -> ImportStatus.WARNING; + case ERROR -> ImportStatus.ERROR; + }); + Stats stats = report.getStats(); + if (stats != null) { + summary.setImportCount( + new ImportCount( + stats.getCreated(), stats.getUpdated(), stats.getIgnored(), stats.getDeleted())); + } + return summary; } private RequestCallback createRequestCallback( diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationServiceTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationServiceTest.java new file mode 100644 index 000000000000..5f9100f73af6 --- /dev/null +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationServiceTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.webapi.controller.tracker.sync; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.hisp.dhis.dxf2.importsummary.ImportStatus; +import org.hisp.dhis.dxf2.importsummary.ImportSummary; +import org.hisp.dhis.tracker.imports.report.ImportReport; +import org.hisp.dhis.tracker.imports.report.Stats; +import org.hisp.dhis.tracker.imports.report.Status; +import org.junit.jupiter.api.Test; + +class SingleEventDataSynchronizationServiceTest { + + @Test + void shouldMapOkStatusToSuccess() { + ImportReport report = ImportReport.builder().status(Status.OK).build(); + + ImportSummary summary = SingleEventDataSynchronizationService.toImportSummary(report); + + assertEquals(ImportStatus.SUCCESS, summary.getStatus()); + } + + @Test + void shouldMapWarningStatusToWarning() { + ImportReport report = ImportReport.builder().status(Status.WARNING).build(); + + ImportSummary summary = SingleEventDataSynchronizationService.toImportSummary(report); + + assertEquals(ImportStatus.WARNING, summary.getStatus()); + } + + @Test + void shouldMapErrorStatusToError() { + ImportReport report = ImportReport.builder().status(Status.ERROR).build(); + + ImportSummary summary = SingleEventDataSynchronizationService.toImportSummary(report); + + assertEquals(ImportStatus.ERROR, summary.getStatus()); + } + + @Test + void shouldMapStats() { + Stats stats = Stats.builder().created(1).updated(2).deleted(3).ignored(4).build(); + ImportReport report = ImportReport.builder().status(Status.OK).stats(stats).build(); + + ImportSummary summary = SingleEventDataSynchronizationService.toImportSummary(report); + + assertEquals(1, summary.getImportCount().getImported()); + assertEquals(2, summary.getImportCount().getUpdated()); + assertEquals(3, summary.getImportCount().getDeleted()); + assertEquals(4, summary.getImportCount().getIgnored()); + } + + @Test + void shouldLeaveImportCountZeroedWhenStatsAbsent() { + ImportReport report = ImportReport.builder().status(Status.OK).build(); + + ImportSummary summary = SingleEventDataSynchronizationService.toImportSummary(report); + + assertEquals(0, summary.getImportCount().getImported()); + assertEquals(0, summary.getImportCount().getUpdated()); + assertEquals(0, summary.getImportCount().getDeleted()); + assertEquals(0, summary.getImportCount().getIgnored()); + } +}