diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/events/event/csv/CsvEventService.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/events/event/csv/CsvEventService.java index 25f63c322c80..913f2a99b73b 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/events/event/csv/CsvEventService.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/events/event/csv/CsvEventService.java @@ -39,6 +39,12 @@ public interface CsvEventService { void writeEvents(OutputStream outputStream, List events, boolean withHeader) throws IOException; + void writeZip(OutputStream outputStream, List toCompress, boolean withHeader, String file) + throws IOException; + + void writeGzip(OutputStream outputStream, List toCompress, boolean withHeader) + throws IOException; + List readEvents(InputStream inputStream, boolean skipFirst) throws IOException, org.locationtech.jts.io.ParseException; } diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/events/event/csv/DefaultCsvEventService.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/events/event/csv/DefaultCsvEventService.java index eea323c15d57..a51bd54b91c0 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/events/event/csv/DefaultCsvEventService.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/events/event/csv/DefaultCsvEventService.java @@ -105,6 +105,19 @@ public void writeEvents(OutputStream outputStream, List events, boolean w writer.writeValue(outputStream, dataValues); } + @Override + public void writeZip( + OutputStream outputStream, List toCompress, boolean withHeader, String file) + throws IOException { + // in use only in the new tracker endpoints + } + + @Override + public void writeGzip(OutputStream outputStream, List toCompress, boolean withHeader) + throws IOException { + // in use only in the new tracker endpoints + } + @Override public List readEvents(InputStream inputStream, boolean skipFirst) throws IOException, ParseException { diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventExportTestConfiguration.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventExportTestConfiguration.java new file mode 100644 index 000000000000..f4a567c3974c --- /dev/null +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventExportTestConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2004-2023, 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: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 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. + * Neither the name of the HISP project 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.export; + +import static org.mockito.Mockito.mock; + +import org.hisp.dhis.dxf2.events.event.EventService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; + +@Configuration +class TrackerEventExportTestConfiguration { + + @Primary + @Profile("TrackerEventExport") + @Bean + EventService eventService() { + return mock(EventService.class); + } +} diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventsExportControllerByIdTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventsExportControllerByIdTest.java new file mode 100644 index 000000000000..55bef378c3d8 --- /dev/null +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventsExportControllerByIdTest.java @@ -0,0 +1,478 @@ +/* + * Copyright (c) 2004-2022, 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: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 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. + * Neither the name of the HISP project 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.export; + +import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertFirstRelationship; +import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertHasMember; +import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertHasNoMember; +import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertHasOnlyMembers; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Date; +import java.util.Set; +import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.common.ValueType; +import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.eventdatavalue.EventDataValue; +import org.hisp.dhis.jsontree.JsonList; +import org.hisp.dhis.jsontree.JsonObject; +import org.hisp.dhis.organisationunit.OrganisationUnit; +import org.hisp.dhis.program.Program; +import org.hisp.dhis.program.ProgramInstance; +import org.hisp.dhis.program.ProgramStage; +import org.hisp.dhis.program.ProgramStageInstance; +import org.hisp.dhis.program.ProgramStatus; +import org.hisp.dhis.program.UserInfoSnapshot; +import org.hisp.dhis.relationship.Relationship; +import org.hisp.dhis.relationship.RelationshipEntity; +import org.hisp.dhis.relationship.RelationshipItem; +import org.hisp.dhis.relationship.RelationshipType; +import org.hisp.dhis.security.acl.AccessStringHelper; +import org.hisp.dhis.trackedentity.TrackedEntityInstance; +import org.hisp.dhis.trackedentity.TrackedEntityType; +import org.hisp.dhis.user.User; +import org.hisp.dhis.user.sharing.UserAccess; +import org.hisp.dhis.webapi.DhisControllerConvenienceTest; +import org.hisp.dhis.webapi.controller.tracker.JsonRelationship; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +class TrackerEventsExportControllerByIdTest extends DhisControllerConvenienceTest { + + @Autowired private IdentifiableObjectManager manager; + + private OrganisationUnit orgUnit; + + private OrganisationUnit anotherOrgUnit; + + private Program program; + + private ProgramStage programStage; + + private User owner; + + private User user; + + private TrackedEntityType trackedEntityType; + + @BeforeEach + void setUp() { + owner = createUser("owner"); + + orgUnit = createOrganisationUnit('A'); + orgUnit.getSharing().setOwner(owner); + manager.save(orgUnit, false); + + anotherOrgUnit = createOrganisationUnit('B'); + anotherOrgUnit.getSharing().setOwner(owner); + manager.save(anotherOrgUnit, false); + + user = createUserWithId("tester", CodeGenerator.generateUid()); + user.addOrganisationUnit(orgUnit); + user.setTeiSearchOrganisationUnits(Set.of(orgUnit)); + this.userService.updateUser(user); + + program = createProgram('A'); + program.addOrganisationUnit(orgUnit); + program.getSharing().setOwner(owner); + program.getSharing().addUserAccess(userAccess()); + manager.save(program, false); + + programStage = createProgramStage('A', program); + programStage.getSharing().setOwner(owner); + programStage.getSharing().addUserAccess(userAccess()); + manager.save(programStage, false); + + trackedEntityType = trackedEntityTypeAccessible(); + } + + @Test + void getEventById() { + TrackedEntityInstance to = trackedEntityInstance(); + ProgramStageInstance from = programStageInstance(programInstance(to)); + relationship(from, to); + + JsonObject json = GET("/tracker/events/{id}", from.getUid()).content(HttpStatus.OK); + + assertDefaultResponse(json, from); + } + + @Test + void getEventByIdWithFields() { + TrackedEntityInstance tei = trackedEntityInstance(); + ProgramStageInstance event = programStageInstance(programInstance(tei)); + + JsonObject json = + GET("/tracker/events/{id}?fields=orgUnit,status", event.getUid()).content(HttpStatus.OK); + + assertFalse(json.isEmpty()); + assertHasOnlyMembers(json, "orgUnit", "status"); + } + + @Test + void getEventByIdWithFieldsRelationships() { + TrackedEntityInstance to = trackedEntityInstance(); + ProgramStageInstance from = programStageInstance(programInstance(to)); + Relationship r = relationship(from, to); + + JsonList relationships = + GET("/tracker/events/{id}?fields=relationships", from.getUid()) + .content(HttpStatus.OK) + .getList("relationships", JsonRelationship.class); + + assertFalse(relationships.isEmpty()); + assertEquals(1, relationships.size()); + JsonRelationship relationship = assertFirstRelationship(r, relationships); + assertEventWithinRelationship(from, relationship.getFrom()); + assertTrackedEntityWithinRelationship(to, relationship.getTo()); + } + + @Test + void getEventByIdRelationshipsNoAccessToRelationshipType() { + TrackedEntityInstance to = trackedEntityInstance(); + ProgramStageInstance from = programStageInstance(programInstance(to)); + relationship(relationshipTypeNotAccessible(), from, to); + this.switchContextToUser(user); + + JsonList relationships = + GET("/tracker/events/{id}?fields=relationships", from.getUid()) + .content(HttpStatus.OK) + .getList("relationships", JsonRelationship.class); + + assertEquals(0, relationships.size()); + } + + @Test + void getEventByIdRelationshipsNoAccessToRelationshipItemTo() { + TrackedEntityType type = trackedEntityTypeNotAccessible(); + TrackedEntityInstance to = trackedEntityInstance(type); + ProgramStageInstance from = programStageInstance(programInstance(to)); + relationship(from, to); + this.switchContextToUser(user); + + JsonList relationships = + GET("/tracker/events/{id}?fields=relationships", from.getUid()) + .content(HttpStatus.OK) + .getList("relationships", JsonRelationship.class); + + assertEquals(0, relationships.size()); + } + + @Test + void getEventByIdRelationshipsNoAccessToBothRelationshipItems() { + TrackedEntityInstance to = trackedEntityInstanceNotInSearchScope(); + ProgramStageInstance from = programStageInstance(programInstance(to)); + relationship(from, to); + this.switchContextToUser(user); + + assertTrue( + GET("/tracker/events/{id}", from.getUid()) + .error(HttpStatus.CONFLICT) + .getMessage() + .contains("OWNERSHIP_ACCESS_DENIED")); + } + + @Test + void getEventByIdRelationshipsNoAccessToRelationshipItemFrom() { + TrackedEntityType type = trackedEntityTypeNotAccessible(); + TrackedEntityInstance from = trackedEntityInstance(type); + ProgramStageInstance to = programStageInstance(programInstance(from)); + relationship(from, to); + this.switchContextToUser(user); + + JsonList relationships = + GET("/tracker/events/{id}?fields=relationships", to.getUid()) + .content(HttpStatus.OK) + .getList("relationships", JsonRelationship.class); + + assertEquals(0, relationships.size()); + } + + @Test + void getEventByIdContainsCreatedByAndUpdateByInDataValues() { + + TrackedEntityInstance tei = trackedEntityInstance(); + ProgramInstance programInstance = programInstance(tei); + ProgramStageInstance programStageInstance = programStageInstance(programInstance); + programStageInstance.setCreatedByUserInfo(UserInfoSnapshot.from(user)); + programStageInstance.setLastUpdatedByUserInfo(UserInfoSnapshot.from(user)); + EventDataValue eventDataValue = new EventDataValue(); + eventDataValue.setValue("6"); + DataElement dataElement = createDataElement('A'); + dataElement.setValueType(ValueType.NUMBER); + manager.save(dataElement); + eventDataValue.setDataElement(dataElement.getUid()); + eventDataValue.setCreatedByUserInfo(UserInfoSnapshot.from(user)); + eventDataValue.setLastUpdatedByUserInfo(UserInfoSnapshot.from(user)); + Set eventDataValues = Set.of(eventDataValue); + programStageInstance.setEventDataValues(eventDataValues); + manager.save(programStageInstance); + + JsonObject event = + GET("/tracker/events/{id}", programStageInstance.getUid()).content(HttpStatus.OK); + + assertTrue(event.isObject()); + assertFalse(event.isEmpty()); + assertEquals(programStageInstance.getUid(), event.getString("event").string()); + assertEquals(programInstance.getUid(), event.getString("enrollment").string()); + assertEquals(orgUnit.getUid(), event.getString("orgUnit").string()); + assertEquals(user.getUsername(), event.getString("createdBy.username").string()); + assertEquals(user.getUsername(), event.getString("updatedBy.username").string()); + assertFalse(event.getArray("dataValues").isEmpty()); + assertEquals( + user.getUsername(), + event.getArray("dataValues").getObject(0).getString("createdBy.username").string()); + assertEquals( + user.getUsername(), + event.getArray("dataValues").getObject(0).getString("updatedBy.username").string()); + } + + @Test + void getEventByIdNotFound() { + assertEquals( + "Event not found for uid: Hq3Kc6HK4OZ", + GET("/tracker/events/Hq3Kc6HK4OZ").error(HttpStatus.NOT_FOUND).getMessage()); + } + + private TrackedEntityType trackedEntityTypeAccessible() { + TrackedEntityType type = trackedEntityType('A'); + type.getSharing().addUserAccess(userAccess()); + manager.save(type, false); + return type; + } + + private TrackedEntityType trackedEntityTypeNotAccessible() { + TrackedEntityType type = trackedEntityType('B'); + manager.save(type, false); + return type; + } + + private TrackedEntityType trackedEntityType(char uniqueChar) { + TrackedEntityType type = createTrackedEntityType(uniqueChar); + type.getSharing().setOwner(owner); + type.getSharing().setPublicAccess(AccessStringHelper.DEFAULT); + return type; + } + + private TrackedEntityInstance trackedEntityInstance() { + TrackedEntityInstance tei = trackedEntityInstance(orgUnit); + manager.save(tei, false); + return tei; + } + + private TrackedEntityInstance trackedEntityInstanceNotInSearchScope() { + TrackedEntityInstance tei = trackedEntityInstance(anotherOrgUnit); + manager.save(tei, false); + return tei; + } + + private TrackedEntityInstance trackedEntityInstance(TrackedEntityType trackedEntityType) { + TrackedEntityInstance tei = trackedEntityInstance(orgUnit, trackedEntityType); + manager.save(tei, false); + return tei; + } + + private TrackedEntityInstance trackedEntityInstance(OrganisationUnit orgUnit) { + return trackedEntityInstance(orgUnit, trackedEntityType); + } + + private TrackedEntityInstance trackedEntityInstance( + OrganisationUnit orgUnit, TrackedEntityType trackedEntityType) { + TrackedEntityInstance tei = createTrackedEntityInstance(orgUnit); + tei.setTrackedEntityType(trackedEntityType); + tei.getSharing().setPublicAccess(AccessStringHelper.DEFAULT); + tei.getSharing().setOwner(owner); + return tei; + } + + private ProgramInstance programInstance(TrackedEntityInstance tei) { + ProgramInstance programInstance = new ProgramInstance(program, tei, tei.getOrganisationUnit()); + programInstance.setAutoFields(); + programInstance.setEnrollmentDate(new Date()); + programInstance.setIncidentDate(new Date()); + programInstance.setStatus(ProgramStatus.COMPLETED); + manager.save(programInstance); + return programInstance; + } + + private ProgramStageInstance programStageInstance(ProgramInstance programInstance) { + ProgramStageInstance programStageInstance = + new ProgramStageInstance( + programInstance, programStage, programInstance.getOrganisationUnit()); + programStageInstance.setAutoFields(); + manager.save(programStageInstance); + return programStageInstance; + } + + private UserAccess userAccess() { + UserAccess a = new UserAccess(); + a.setUser(user); + a.setAccess(AccessStringHelper.FULL); + return a; + } + + private RelationshipType relationshipTypeAccessible( + RelationshipEntity from, RelationshipEntity to) { + RelationshipType type = relationshipType(from, to); + type.getSharing().addUserAccess(userAccess()); + manager.save(type, false); + return type; + } + + private RelationshipType relationshipTypeNotAccessible() { + return relationshipType( + RelationshipEntity.PROGRAM_STAGE_INSTANCE, RelationshipEntity.TRACKED_ENTITY_INSTANCE); + } + + private RelationshipType relationshipType(RelationshipEntity from, RelationshipEntity to) { + RelationshipType type = createRelationshipType('A'); + type.getFromConstraint().setRelationshipEntity(from); + type.getToConstraint().setRelationshipEntity(to); + type.getSharing().setOwner(owner); + type.getSharing().setPublicAccess(AccessStringHelper.DEFAULT); + manager.save(type, false); + return type; + } + + private Relationship relationship(ProgramStageInstance from, TrackedEntityInstance to) { + return relationship( + relationshipTypeAccessible( + RelationshipEntity.PROGRAM_STAGE_INSTANCE, RelationshipEntity.TRACKED_ENTITY_INSTANCE), + from, + to); + } + + private Relationship relationship( + RelationshipType type, ProgramStageInstance from, TrackedEntityInstance to) { + Relationship r = new Relationship(); + + RelationshipItem fromItem = new RelationshipItem(); + fromItem.setProgramStageInstance(from); + from.getRelationshipItems().add(fromItem); + fromItem.setRelationship(r); + r.setFrom(fromItem); + + RelationshipItem toItem = new RelationshipItem(); + toItem.setTrackedEntityInstance(to); + to.getRelationshipItems().add(toItem); + r.setTo(toItem); + toItem.setRelationship(r); + + r.setRelationshipType(type); + r.setKey(type.getUid()); + r.setInvertedKey(type.getUid()); + r.setAutoFields(); + r.getSharing().setOwner(owner); + manager.save(r, false); + return r; + } + + private Relationship relationship(TrackedEntityInstance from, ProgramStageInstance to) { + Relationship r = new Relationship(); + + RelationshipItem fromItem = new RelationshipItem(); + fromItem.setTrackedEntityInstance(from); + from.getRelationshipItems().add(fromItem); + r.setFrom(fromItem); + fromItem.setRelationship(r); + + RelationshipItem toItem = new RelationshipItem(); + toItem.setProgramStageInstance(to); + to.getRelationshipItems().add(toItem); + r.setTo(toItem); + toItem.setRelationship(r); + + RelationshipType type = + relationshipTypeAccessible( + RelationshipEntity.PROGRAM_STAGE_INSTANCE, RelationshipEntity.TRACKED_ENTITY_INSTANCE); + r.setRelationshipType(type); + r.setKey(type.getUid()); + r.setInvertedKey(type.getUid()); + + r.setAutoFields(); + r.getSharing().setOwner(owner); + manager.save(r, false); + return r; + } + + private void assertEventWithinRelationship(ProgramStageInstance expected, JsonObject json) { + JsonObject jsonEvent = json.getObject("event"); + assertFalse(jsonEvent.isEmpty(), "event should not be empty"); + assertEquals(expected.getUid(), jsonEvent.getString("event").string(), "event UID"); + assertFalse(jsonEvent.has("status")); + assertFalse(jsonEvent.has("orgUnit")); + assertFalse(jsonEvent.has("programStage")); + assertFalse( + jsonEvent.has("relationships"), "relationships is not returned within relationship items"); + } + + private void assertTrackedEntityWithinRelationship( + TrackedEntityInstance expected, JsonObject json) { + JsonObject jsonTEI = json.getObject("trackedEntity"); + assertFalse(jsonTEI.isEmpty(), "trackedEntity should not be empty"); + assertEquals( + expected.getUid(), jsonTEI.getString("trackedEntity").string(), "trackedEntity UID"); + assertFalse(jsonTEI.has("trackedEntityType")); + assertFalse(jsonTEI.has("orgUnit")); + assertFalse( + jsonTEI.has("relationships"), "relationships is not returned within relationship items"); + assertTrue(jsonTEI.getArray("attributes").isEmpty()); + } + + private void assertDefaultResponse(JsonObject json, ProgramStageInstance programStageInstance) { + // note that some fields are not included in the response because they + // are not part of the setup + // i.e attributeOptionCombo, ... + assertTrue(json.isObject()); + assertFalse(json.isEmpty()); + assertEquals(programStageInstance.getUid(), json.getString("event").string(), "event UID"); + assertEquals("ACTIVE", json.getString("status").string()); + assertEquals(program.getUid(), json.getString("program").string()); + assertEquals(programStage.getUid(), json.getString("programStage").string()); + assertEquals( + programStageInstance.getProgramInstance().getUid(), json.getString("enrollment").string()); + assertEquals(orgUnit.getUid(), json.getString("orgUnit").string()); + assertEquals(orgUnit.getName(), json.getString("orgUnitName").string()); + assertFalse(json.getBoolean("followup").booleanValue()); + assertFalse(json.getBoolean("deleted").booleanValue()); + assertHasMember(json, "createdAt"); + assertHasMember(json, "createdAtClient"); + assertHasMember(json, "updatedAt"); + assertHasMember(json, "updatedAtClient"); + assertHasMember(json, "dataValues"); + assertHasMember(json, "notes"); + assertHasNoMember(json, "attributeOptionCombo"); + assertHasNoMember(json, "attributeCategoryOptions"); + assertHasNoMember(json, "relationships"); + } +} diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventsExportControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventsExportControllerTest.java index 7b3f174b546b..46cbb8d909b8 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventsExportControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventsExportControllerTest.java @@ -27,74 +27,54 @@ */ package org.hisp.dhis.webapi.controller.tracker.export; -import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertFirstRelationship; -import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertHasMember; -import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertHasNoMember; -import static org.hisp.dhis.webapi.controller.tracker.JsonAssertions.assertHasOnlyMembers; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; -import java.util.Date; import java.util.Set; +import java.util.stream.Stream; import org.hisp.dhis.common.CodeGenerator; import org.hisp.dhis.common.IdentifiableObjectManager; -import org.hisp.dhis.common.ValueType; import org.hisp.dhis.dataelement.DataElement; -import org.hisp.dhis.eventdatavalue.EventDataValue; -import org.hisp.dhis.jsontree.JsonList; -import org.hisp.dhis.jsontree.JsonObject; +import org.hisp.dhis.dxf2.events.event.EventService; +import org.hisp.dhis.dxf2.events.event.Events; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.program.Program; -import org.hisp.dhis.program.ProgramInstance; import org.hisp.dhis.program.ProgramStage; -import org.hisp.dhis.program.ProgramStageInstance; -import org.hisp.dhis.program.ProgramStatus; -import org.hisp.dhis.program.UserInfoSnapshot; -import org.hisp.dhis.relationship.Relationship; -import org.hisp.dhis.relationship.RelationshipEntity; -import org.hisp.dhis.relationship.RelationshipItem; -import org.hisp.dhis.relationship.RelationshipType; import org.hisp.dhis.security.acl.AccessStringHelper; -import org.hisp.dhis.trackedentity.TrackedEntityInstance; -import org.hisp.dhis.trackedentity.TrackedEntityType; import org.hisp.dhis.user.User; import org.hisp.dhis.user.sharing.UserAccess; import org.hisp.dhis.webapi.DhisControllerConvenienceTest; -import org.hisp.dhis.webapi.controller.tracker.JsonRelationship; +import org.hisp.dhis.webapi.utils.ContextUtils; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +@ContextConfiguration(classes = TrackerEventExportTestConfiguration.class) +@ActiveProfiles("TrackerEventExport") class TrackerEventsExportControllerTest extends DhisControllerConvenienceTest { - @Autowired private IdentifiableObjectManager manager; - private OrganisationUnit orgUnit; - - private OrganisationUnit anotherOrgUnit; - - private Program program; - - private ProgramStage programStage; - - private User owner; + @Autowired private EventService eventService; private User user; - private TrackedEntityType trackedEntityType; - @BeforeEach void setUp() { - owner = createUser("owner"); + user = createUser("owner"); - orgUnit = createOrganisationUnit('A'); - orgUnit.getSharing().setOwner(owner); + OrganisationUnit orgUnit = createOrganisationUnit('A'); + orgUnit.getSharing().setOwner(user); manager.save(orgUnit, false); - anotherOrgUnit = createOrganisationUnit('B'); - anotherOrgUnit.getSharing().setOwner(owner); + OrganisationUnit anotherOrgUnit = createOrganisationUnit('B'); + anotherOrgUnit.getSharing().setOwner(user); manager.save(anotherOrgUnit, false); user = createUserWithId("tester", CodeGenerator.generateUid()); @@ -102,236 +82,90 @@ void setUp() { user.setTeiSearchOrganisationUnits(Set.of(orgUnit)); this.userService.updateUser(user); - program = createProgram('A'); + Program program = createProgram('A'); program.addOrganisationUnit(orgUnit); - program.getSharing().setOwner(owner); + program.getSharing().setOwner(user); program.getSharing().addUserAccess(userAccess()); manager.save(program, false); - programStage = createProgramStage('A', program); - programStage.getSharing().setOwner(owner); + ProgramStage programStage = createProgramStage('A', program); + programStage.getSharing().setOwner(user); programStage.getSharing().addUserAccess(userAccess()); manager.save(programStage, false); - trackedEntityType = trackedEntityTypeAccessible(); - } - - @Test - void getEventById() { - TrackedEntityInstance to = trackedEntityInstance(); - ProgramStageInstance from = programStageInstance(programInstance(to)); - relationship(from, to); - - JsonObject json = GET("/tracker/events/{id}", from.getUid()).content(HttpStatus.OK); - - assertDefaultResponse(json, from); - } - - @Test - void getEventByIdWithFields() { - TrackedEntityInstance tei = trackedEntityInstance(); - ProgramStageInstance event = programStageInstance(programInstance(tei)); - - JsonObject json = - GET("/tracker/events/{id}?fields=orgUnit,status", event.getUid()).content(HttpStatus.OK); - - assertFalse(json.isEmpty()); - assertHasOnlyMembers(json, "orgUnit", "status"); - } - - @Test - void getEventByIdWithFieldsRelationships() { - TrackedEntityInstance to = trackedEntityInstance(); - ProgramStageInstance from = programStageInstance(programInstance(to)); - Relationship r = relationship(from, to); - - JsonList relationships = - GET("/tracker/events/{id}?fields=relationships", from.getUid()) - .content(HttpStatus.OK) - .getList("relationships", JsonRelationship.class); - - assertFalse(relationships.isEmpty()); - assertEquals(1, relationships.size()); - JsonRelationship relationship = assertFirstRelationship(r, relationships); - assertEventWithinRelationship(from, relationship.getFrom()); - assertTrackedEntityWithinRelationship(to, relationship.getTo()); - } - - @Test - void getEventByIdRelationshipsNoAccessToRelationshipType() { - TrackedEntityInstance to = trackedEntityInstance(); - ProgramStageInstance from = programStageInstance(programInstance(to)); - relationship(relationshipTypeNotAccessible(), from, to); - this.switchContextToUser(user); - - JsonList relationships = - GET("/tracker/events/{id}?fields=relationships", from.getUid()) - .content(HttpStatus.OK) - .getList("relationships", JsonRelationship.class); - - assertEquals(0, relationships.size()); - } - - @Test - void getEventByIdRelationshipsNoAccessToRelationshipItemTo() { - TrackedEntityType type = trackedEntityTypeNotAccessible(); - TrackedEntityInstance to = trackedEntityInstance(type); - ProgramStageInstance from = programStageInstance(programInstance(to)); - relationship(from, to); - this.switchContextToUser(user); - - JsonList relationships = - GET("/tracker/events/{id}?fields=relationships", from.getUid()) - .content(HttpStatus.OK) - .getList("relationships", JsonRelationship.class); - - assertEquals(0, relationships.size()); - } - - @Test - void getEventByIdRelationshipsNoAccessToBothRelationshipItems() { - TrackedEntityInstance to = trackedEntityInstanceNotInSearchScope(); - ProgramStageInstance from = programStageInstance(programInstance(to)); - relationship(from, to); - this.switchContextToUser(user); - - assertTrue( - GET("/tracker/events/{id}", from.getUid()) - .error(HttpStatus.CONFLICT) - .getMessage() - .contains("OWNERSHIP_ACCESS_DENIED")); - } - - @Test - void getEventByIdRelationshipsNoAccessToRelationshipItemFrom() { - TrackedEntityType type = trackedEntityTypeNotAccessible(); - TrackedEntityInstance from = trackedEntityInstance(type); - ProgramStageInstance to = programStageInstance(programInstance(from)); - relationship(from, to); - this.switchContextToUser(user); - - JsonList relationships = - GET("/tracker/events/{id}?fields=relationships", to.getUid()) - .content(HttpStatus.OK) - .getList("relationships", JsonRelationship.class); - - assertEquals(0, relationships.size()); - } - - @Test - void getEventByIdContainsCreatedByAndUpdateByInDataValues() { - - TrackedEntityInstance tei = trackedEntityInstance(); - ProgramInstance programInstance = programInstance(tei); - ProgramStageInstance programStageInstance = programStageInstance(programInstance); - programStageInstance.setCreatedByUserInfo(UserInfoSnapshot.from(user)); - programStageInstance.setLastUpdatedByUserInfo(UserInfoSnapshot.from(user)); - EventDataValue eventDataValue = new EventDataValue(); - eventDataValue.setValue("6"); - DataElement dataElement = createDataElement('A'); - dataElement.setValueType(ValueType.NUMBER); - manager.save(dataElement); - eventDataValue.setDataElement(dataElement.getUid()); - eventDataValue.setCreatedByUserInfo(UserInfoSnapshot.from(user)); - eventDataValue.setLastUpdatedByUserInfo(UserInfoSnapshot.from(user)); - Set eventDataValues = Set.of(eventDataValue); - programStageInstance.setEventDataValues(eventDataValues); - manager.save(programStageInstance); - - JsonObject event = - GET("/tracker/events/{id}", programStageInstance.getUid()).content(HttpStatus.OK); - - assertTrue(event.isObject()); - assertFalse(event.isEmpty()); - assertEquals(programStageInstance.getUid(), event.getString("event").string()); - assertEquals(programInstance.getUid(), event.getString("enrollment").string()); - assertEquals(orgUnit.getUid(), event.getString("orgUnit").string()); - assertEquals(user.getUsername(), event.getString("createdBy.username").string()); - assertEquals(user.getUsername(), event.getString("updatedBy.username").string()); - assertFalse(event.getArray("dataValues").isEmpty()); - assertEquals( - user.getUsername(), - event.getArray("dataValues").getObject(0).getString("createdBy.username").string()); - assertEquals( - user.getUsername(), - event.getArray("dataValues").getObject(0).getString("updatedBy.username").string()); - } - - @Test - void getEventByIdNotFound() { - assertEquals( - "Event not found for uid: Hq3Kc6HK4OZ", - GET("/tracker/events/Hq3Kc6HK4OZ").error(HttpStatus.NOT_FOUND).getMessage()); - } - - private TrackedEntityType trackedEntityTypeAccessible() { - TrackedEntityType type = trackedEntityType('A'); - type.getSharing().addUserAccess(userAccess()); - manager.save(type, false); - return type; - } - - private TrackedEntityType trackedEntityTypeNotAccessible() { - TrackedEntityType type = trackedEntityType('B'); - manager.save(type, false); - return type; - } - - private TrackedEntityType trackedEntityType(char uniqueChar) { - TrackedEntityType type = createTrackedEntityType(uniqueChar); - type.getSharing().setOwner(owner); - type.getSharing().setPublicAccess(AccessStringHelper.DEFAULT); - return type; - } - - private TrackedEntityInstance trackedEntityInstance() { - TrackedEntityInstance tei = trackedEntityInstance(orgUnit); - manager.save(tei, false); - return tei; - } - - private TrackedEntityInstance trackedEntityInstanceNotInSearchScope() { - TrackedEntityInstance tei = trackedEntityInstance(anotherOrgUnit); - manager.save(tei, false); - return tei; - } - - private TrackedEntityInstance trackedEntityInstance(TrackedEntityType trackedEntityType) { - TrackedEntityInstance tei = trackedEntityInstance(orgUnit, trackedEntityType); - manager.save(tei, false); - return tei; - } - - private TrackedEntityInstance trackedEntityInstance(OrganisationUnit orgUnit) { - return trackedEntityInstance(orgUnit, trackedEntityType); - } - - private TrackedEntityInstance trackedEntityInstance( - OrganisationUnit orgUnit, TrackedEntityType trackedEntityType) { - TrackedEntityInstance tei = createTrackedEntityInstance(orgUnit); - tei.setTrackedEntityType(trackedEntityType); - tei.getSharing().setPublicAccess(AccessStringHelper.DEFAULT); - tei.getSharing().setOwner(owner); - return tei; - } - - private ProgramInstance programInstance(TrackedEntityInstance tei) { - ProgramInstance programInstance = new ProgramInstance(program, tei, tei.getOrganisationUnit()); - programInstance.setAutoFields(); - programInstance.setEnrollmentDate(new Date()); - programInstance.setIncidentDate(new Date()); - programInstance.setStatus(ProgramStatus.COMPLETED); - manager.save(programInstance); - return programInstance; - } - - private ProgramStageInstance programStageInstance(ProgramInstance programInstance) { - ProgramStageInstance programStageInstance = - new ProgramStageInstance( - programInstance, programStage, programInstance.getOrganisationUnit()); - programStageInstance.setAutoFields(); - manager.save(programStageInstance); - return programStageInstance; + DataElement de = createDataElement('A'); + de.getSharing().setOwner(user); + manager.save(de, false); + } + + static Stream + shouldMatchContentTypeAndAttachmentWhenEndpointForCompressedEventJsonIsInvoked() { + return Stream.of( + arguments( + "/tracker/events.json.zip?attachment=file.json.zip", + "application/json+zip", + "attachment; filename=file.json.zip", + "binary"), + arguments( + "/tracker/events.json.zip", + "application/json+zip", + "attachment; filename=events.json.zip", + "binary"), + arguments( + "/tracker/events.json.gz?attachment=file.json.gz", + "application/json+gzip", + "attachment; filename=file.json.gz", + "binary"), + arguments( + "/tracker/events.json.gz", + "application/json+gzip", + "attachment; filename=events.json.gz", + "binary"), + arguments( + "/tracker/events.csv", + "application/csv; charset=UTF-8", + "attachment; filename=events.csv", + null), + arguments( + "/tracker/events.csv?attachment=file.csv", + "application/csv; charset=UTF-8", + "attachment; filename=file.csv", + null), + arguments( + "/tracker/events.csv.gz", + "application/csv+gzip", + "attachment; filename=events.csv.gz", + "binary"), + arguments( + "/tracker/events.csv.gz?attachment=file.csv.gz", + "application/csv+gzip", + "attachment; filename=file.csv.gz", + "binary"), + arguments( + "/tracker/events.csv.zip", + "application/csv+zip", + "attachment; filename=events.csv.zip", + "binary"), + arguments( + "/tracker/events.csv.zip?attachment=file.csv.zip", + "application/csv+zip", + "attachment; filename=file.csv.zip", + "binary")); + } + + @ParameterizedTest + @MethodSource + void shouldMatchContentTypeAndAttachmentWhenEndpointForCompressedEventJsonIsInvoked( + String url, String expectedContentType, String expectedAttachment, String encoding) { + + when(eventService.getEvents(any())).thenReturn(new Events()); + injectSecurityContext(user); + + HttpResponse res = GET(url); + assertEquals(HttpStatus.OK, res.status()); + assertEquals(expectedContentType, res.header("Content-Type")); + assertEquals(expectedAttachment, res.header(ContextUtils.HEADER_CONTENT_DISPOSITION)); + assertEquals(encoding, res.header(ContextUtils.HEADER_CONTENT_TRANSFER_ENCODING)); } private UserAccess userAccess() { @@ -340,139 +174,4 @@ private UserAccess userAccess() { a.setAccess(AccessStringHelper.FULL); return a; } - - private RelationshipType relationshipTypeAccessible( - RelationshipEntity from, RelationshipEntity to) { - RelationshipType type = relationshipType(from, to); - type.getSharing().addUserAccess(userAccess()); - manager.save(type, false); - return type; - } - - private RelationshipType relationshipTypeNotAccessible() { - return relationshipType( - RelationshipEntity.PROGRAM_STAGE_INSTANCE, RelationshipEntity.TRACKED_ENTITY_INSTANCE); - } - - private RelationshipType relationshipType(RelationshipEntity from, RelationshipEntity to) { - RelationshipType type = createRelationshipType('A'); - type.getFromConstraint().setRelationshipEntity(from); - type.getToConstraint().setRelationshipEntity(to); - type.getSharing().setOwner(owner); - type.getSharing().setPublicAccess(AccessStringHelper.DEFAULT); - manager.save(type, false); - return type; - } - - private Relationship relationship(ProgramStageInstance from, TrackedEntityInstance to) { - return relationship( - relationshipTypeAccessible( - RelationshipEntity.PROGRAM_STAGE_INSTANCE, RelationshipEntity.TRACKED_ENTITY_INSTANCE), - from, - to); - } - - private Relationship relationship( - RelationshipType type, ProgramStageInstance from, TrackedEntityInstance to) { - Relationship r = new Relationship(); - - RelationshipItem fromItem = new RelationshipItem(); - fromItem.setProgramStageInstance(from); - from.getRelationshipItems().add(fromItem); - fromItem.setRelationship(r); - r.setFrom(fromItem); - - RelationshipItem toItem = new RelationshipItem(); - toItem.setTrackedEntityInstance(to); - to.getRelationshipItems().add(toItem); - r.setTo(toItem); - toItem.setRelationship(r); - - r.setRelationshipType(type); - r.setKey(type.getUid()); - r.setInvertedKey(type.getUid()); - r.setAutoFields(); - r.getSharing().setOwner(owner); - manager.save(r, false); - return r; - } - - private Relationship relationship(TrackedEntityInstance from, ProgramStageInstance to) { - Relationship r = new Relationship(); - - RelationshipItem fromItem = new RelationshipItem(); - fromItem.setTrackedEntityInstance(from); - from.getRelationshipItems().add(fromItem); - r.setFrom(fromItem); - fromItem.setRelationship(r); - - RelationshipItem toItem = new RelationshipItem(); - toItem.setProgramStageInstance(to); - to.getRelationshipItems().add(toItem); - r.setTo(toItem); - toItem.setRelationship(r); - - RelationshipType type = - relationshipTypeAccessible( - RelationshipEntity.PROGRAM_STAGE_INSTANCE, RelationshipEntity.TRACKED_ENTITY_INSTANCE); - r.setRelationshipType(type); - r.setKey(type.getUid()); - r.setInvertedKey(type.getUid()); - - r.setAutoFields(); - r.getSharing().setOwner(owner); - manager.save(r, false); - return r; - } - - private void assertEventWithinRelationship(ProgramStageInstance expected, JsonObject json) { - JsonObject jsonEvent = json.getObject("event"); - assertFalse(jsonEvent.isEmpty(), "event should not be empty"); - assertEquals(expected.getUid(), jsonEvent.getString("event").string(), "event UID"); - assertFalse(jsonEvent.has("status")); - assertFalse(jsonEvent.has("orgUnit")); - assertFalse(jsonEvent.has("programStage")); - assertFalse( - jsonEvent.has("relationships"), "relationships is not returned within relationship items"); - } - - private void assertTrackedEntityWithinRelationship( - TrackedEntityInstance expected, JsonObject json) { - JsonObject jsonTEI = json.getObject("trackedEntity"); - assertFalse(jsonTEI.isEmpty(), "trackedEntity should not be empty"); - assertEquals( - expected.getUid(), jsonTEI.getString("trackedEntity").string(), "trackedEntity UID"); - assertFalse(jsonTEI.has("trackedEntityType")); - assertFalse(jsonTEI.has("orgUnit")); - assertFalse( - jsonTEI.has("relationships"), "relationships is not returned within relationship items"); - assertTrue(jsonTEI.getArray("attributes").isEmpty()); - } - - private void assertDefaultResponse(JsonObject json, ProgramStageInstance programStageInstance) { - // note that some fields are not included in the response because they - // are not part of the setup - // i.e attributeOptionCombo, ... - assertTrue(json.isObject()); - assertFalse(json.isEmpty()); - assertEquals(programStageInstance.getUid(), json.getString("event").string(), "event UID"); - assertEquals("ACTIVE", json.getString("status").string()); - assertEquals(program.getUid(), json.getString("program").string()); - assertEquals(programStage.getUid(), json.getString("programStage").string()); - assertEquals( - programStageInstance.getProgramInstance().getUid(), json.getString("enrollment").string()); - assertEquals(orgUnit.getUid(), json.getString("orgUnit").string()); - assertEquals(orgUnit.getName(), json.getString("orgUnitName").string()); - assertFalse(json.getBoolean("followup").booleanValue()); - assertFalse(json.getBoolean("deleted").booleanValue()); - assertHasMember(json, "createdAt"); - assertHasMember(json, "createdAtClient"); - assertHasMember(json, "updatedAt"); - assertHasMember(json, "updatedAtClient"); - assertHasMember(json, "dataValues"); - assertHasMember(json, "notes"); - assertHasNoMember(json, "attributeOptionCombo"); - assertHasNoMember(json, "attributeCategoryOptions"); - assertHasNoMember(json, "relationships"); - } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/csv/TrackerCsvEventService.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/csv/TrackerCsvEventService.java index 32c6f1dbfe32..bf730974485b 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/csv/TrackerCsvEventService.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/csv/TrackerCsvEventService.java @@ -48,6 +48,7 @@ import org.hisp.dhis.tracker.domain.Event; import org.hisp.dhis.tracker.domain.User; import org.hisp.dhis.util.DateUtils; +import org.hisp.dhis.webapi.controller.tracker.export.CompressionUtil; import org.locationtech.jts.io.ParseException; import org.locationtech.jts.io.WKTReader; import org.springframework.stereotype.Service; @@ -63,60 +64,105 @@ public class TrackerCsvEventService implements CsvEventService { @Override public void writeEvents(OutputStream outputStream, List events, boolean withHeader) throws IOException { + + ObjectWriter writer = getObjectWriter(withHeader); + + writer.writeValue(outputStream, getCsvEventDataValues(events)); + } + + private static CsvEventDataValue map(DataValue value, CsvEventDataValue templateDataValue) { + CsvEventDataValue dataValue = new CsvEventDataValue(templateDataValue); + dataValue.setDataElement(value.getDataElement()); + dataValue.setValue(value.getValue()); + dataValue.setProvidedElsewhere(value.isProvidedElsewhere()); + dataValue.setCreatedAtDataValue( + value.getCreatedAt() == null ? null : value.getCreatedAt().toString()); + dataValue.setUpdatedAtDataValue( + value.getUpdatedAt() == null ? null : value.getUpdatedAt().toString()); + + if (value.getStoredBy() != null) { + dataValue.setStoredBy(value.getStoredBy()); + } + return dataValue; + } + + private static CsvEventDataValue map(Event event) { + CsvEventDataValue templateDataValue = new CsvEventDataValue(); + templateDataValue.setEvent(event.getEvent()); + templateDataValue.setStatus(event.getStatus() != null ? event.getStatus().name() : null); + templateDataValue.setProgram(event.getProgram()); + templateDataValue.setProgramStage(event.getProgramStage()); + templateDataValue.setEnrollment(event.getEnrollment()); + templateDataValue.setOrgUnit(event.getOrgUnit()); + templateDataValue.setOrgUnitName(event.getOrgUnitName()); + templateDataValue.setOccurredAt( + event.getOccurredAt() == null ? null : event.getOccurredAt().toString()); + templateDataValue.setScheduledAt( + event.getScheduledAt() == null ? null : event.getScheduledAt().toString()); + templateDataValue.setFollowup(event.isFollowup()); + templateDataValue.setDeleted(event.isDeleted()); + templateDataValue.setCreatedAt( + event.getCreatedAt() == null ? null : event.getCreatedAt().toString()); + templateDataValue.setCreatedAtClient( + event.getCreatedAtClient() == null ? null : event.getCreatedAtClient().toString()); + templateDataValue.setUpdatedAt( + event.getUpdatedAt() == null ? null : event.getUpdatedAt().toString()); + templateDataValue.setUpdatedAtClient( + event.getUpdatedAtClient() == null ? null : event.getUpdatedAtClient().toString()); + templateDataValue.setCompletedAt( + event.getCompletedAt() == null ? null : event.getCompletedAt().toString()); + templateDataValue.setUpdatedBy( + event.getUpdatedBy() == null ? null : event.getUpdatedBy().getUsername()); + templateDataValue.setStoredBy(event.getStoredBy()); + templateDataValue.setCompletedAt( + event.getCompletedAt() == null ? null : event.getCompletedAt().toString()); + templateDataValue.setCompletedBy(event.getCompletedBy()); + templateDataValue.setAttributeOptionCombo(event.getAttributeOptionCombo()); + templateDataValue.setAttributeCategoryOptions(event.getAttributeCategoryOptions()); + templateDataValue.setAssignedUser( + event.getAssignedUser() == null ? null : event.getAssignedUser().getUsername()); + + if (event.getGeometry() != null) { + templateDataValue.setGeometry(event.getGeometry().toText()); + + if (event.getGeometry().getGeometryType().equals("Point")) { + templateDataValue.setLongitude(event.getGeometry().getCoordinate().x); + templateDataValue.setLatitude(event.getGeometry().getCoordinate().y); + } + } + return templateDataValue; + } + + @Override + public void writeZip( + OutputStream outputStream, List toCompress, boolean withHeader, String file) + throws IOException { + CompressionUtil.writeZip( + outputStream, getCsvEventDataValues(toCompress), getObjectWriter(withHeader), file); + } + + @Override + public void writeGzip(OutputStream outputStream, List toCompress, boolean withHeader) + throws IOException { + CompressionUtil.writeGzip( + outputStream, getCsvEventDataValues(toCompress), getObjectWriter(withHeader)); + } + + private ObjectWriter getObjectWriter(boolean withHeader) { final CsvSchema csvSchema = CSV_MAPPER .schemaFor(CsvEventDataValue.class) .withLineSeparator("\n") .withUseHeader(withHeader); - ObjectWriter writer = CSV_MAPPER.writer(csvSchema.withUseHeader(withHeader)); + return CSV_MAPPER.writer(csvSchema.withUseHeader(withHeader)); + } + private List getCsvEventDataValues(List events) { List dataValues = new ArrayList<>(); for (Event event : events) { - CsvEventDataValue templateDataValue = new CsvEventDataValue(); - templateDataValue.setEvent(event.getEvent()); - templateDataValue.setStatus(event.getStatus() != null ? event.getStatus().name() : null); - templateDataValue.setProgram(event.getProgram()); - templateDataValue.setProgramStage(event.getProgramStage()); - templateDataValue.setEnrollment(event.getEnrollment()); - templateDataValue.setOrgUnit(event.getOrgUnit()); - templateDataValue.setOrgUnitName(event.getOrgUnitName()); - templateDataValue.setOccurredAt( - event.getOccurredAt() == null ? null : event.getOccurredAt().toString()); - templateDataValue.setScheduledAt( - event.getScheduledAt() == null ? null : event.getScheduledAt().toString()); - templateDataValue.setFollowup(event.isFollowup()); - templateDataValue.setDeleted(event.isDeleted()); - templateDataValue.setCreatedAt( - event.getCreatedAt() == null ? null : event.getCreatedAt().toString()); - templateDataValue.setCreatedAtClient( - event.getCreatedAtClient() == null ? null : event.getCreatedAtClient().toString()); - templateDataValue.setUpdatedAt( - event.getUpdatedAt() == null ? null : event.getUpdatedAt().toString()); - templateDataValue.setUpdatedAtClient( - event.getUpdatedAtClient() == null ? null : event.getUpdatedAtClient().toString()); - templateDataValue.setCompletedAt( - event.getCompletedAt() == null ? null : event.getCompletedAt().toString()); - templateDataValue.setUpdatedBy( - event.getUpdatedBy() == null ? null : event.getUpdatedBy().getUsername()); - templateDataValue.setStoredBy(event.getStoredBy()); - templateDataValue.setCompletedAt( - event.getCompletedAt() == null ? null : event.getCompletedAt().toString()); - templateDataValue.setCompletedBy(event.getCompletedBy()); - templateDataValue.setAttributeOptionCombo(event.getAttributeOptionCombo()); - templateDataValue.setAttributeCategoryOptions(event.getAttributeCategoryOptions()); - templateDataValue.setAssignedUser( - event.getAssignedUser() == null ? null : event.getAssignedUser().getUsername()); - - if (event.getGeometry() != null) { - templateDataValue.setGeometry(event.getGeometry().toText()); - - if (event.getGeometry().getGeometryType().equals("Point")) { - templateDataValue.setLongitude(event.getGeometry().getCoordinate().x); - templateDataValue.setLatitude(event.getGeometry().getCoordinate().y); - } - } + CsvEventDataValue templateDataValue = map(event); if (event.getDataValues().isEmpty()) { dataValues.add(templateDataValue); @@ -124,24 +170,10 @@ public void writeEvents(OutputStream outputStream, List events, boolean w } for (DataValue value : event.getDataValues()) { - CsvEventDataValue dataValue = new CsvEventDataValue(templateDataValue); - dataValue.setDataElement(value.getDataElement()); - dataValue.setValue(value.getValue()); - dataValue.setProvidedElsewhere(value.isProvidedElsewhere()); - dataValue.setCreatedAtDataValue( - value.getCreatedAt() == null ? null : value.getCreatedAt().toString()); - dataValue.setUpdatedAtDataValue( - value.getUpdatedAt() == null ? null : value.getUpdatedAt().toString()); - - if (value.getStoredBy() != null) { - dataValue.setStoredBy(value.getStoredBy()); - } - - dataValues.add(dataValue); + dataValues.add(map(value, templateDataValue)); } } - - writer.writeValue(outputStream, dataValues); + return dataValues; } @Override diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/export/CompressionUtil.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/export/CompressionUtil.java new file mode 100644 index 000000000000..f6d66d83e6a2 --- /dev/null +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/export/CompressionUtil.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2004-2023, 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: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 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. + * Neither the name of the HISP project 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.export; + +import com.fasterxml.jackson.databind.ObjectWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.GZIPOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class CompressionUtil { + + private CompressionUtil() { + throw new IllegalStateException( + "Utility class to compress exported objects in Zip o GZip format"); + } + + /** + * @param requestOutputStream Output stream from request + * @param toCompress Objects to compress + * @param objectWriter Object writer from a mapper + * @param attachment Attachment file name + * @param + * @throws IOException + */ + public static void writeZip( + OutputStream requestOutputStream, T toCompress, ObjectWriter objectWriter, String attachment) + throws IOException { + ZipOutputStream outputStream = new ZipOutputStream(requestOutputStream); + outputStream.putNextEntry(new ZipEntry(attachment)); + + objectWriter.writeValue(outputStream, toCompress); + outputStream.close(); + } + + /** + * @param requestOutputStream Output stream from request + * @param toCompress Objects to compress + * @param objectWriter Object writer from a mapper + * @param + * @throws IOException + */ + public static void writeGzip( + OutputStream requestOutputStream, T toCompress, ObjectWriter objectWriter) + throws IOException { + GZIPOutputStream outputStream = new GZIPOutputStream(requestOutputStream); + + objectWriter.writeValue(outputStream, toCompress); + outputStream.close(); + } +} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventsExportController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventsExportController.java index dd9f321fa29e..ec9ebf1b0cc8 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventsExportController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/export/TrackerEventsExportController.java @@ -28,18 +28,24 @@ package org.hisp.dhis.webapi.controller.tracker.export; import static org.hisp.dhis.webapi.controller.tracker.TrackerControllerSupport.RESOURCE_PATH; +import static org.hisp.dhis.webapi.controller.tracker.export.CompressionUtil.writeGzip; +import static org.hisp.dhis.webapi.controller.tracker.export.CompressionUtil.writeZip; import static org.hisp.dhis.webapi.utils.ContextUtils.CONTENT_TYPE_CSV; import static org.hisp.dhis.webapi.utils.ContextUtils.CONTENT_TYPE_CSV_GZIP; +import static org.hisp.dhis.webapi.utils.ContextUtils.CONTENT_TYPE_CSV_ZIP; +import static org.hisp.dhis.webapi.utils.ContextUtils.CONTENT_TYPE_JSON_GZIP; +import static org.hisp.dhis.webapi.utils.ContextUtils.CONTENT_TYPE_JSON_ZIP; import static org.hisp.dhis.webapi.utils.ContextUtils.CONTENT_TYPE_TEXT_CSV; +import static org.hisp.dhis.webapi.utils.ContextUtils.HEADER_CONTENT_TRANSFER_ENCODING; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; import java.io.OutputStream; import java.util.Collections; import java.util.List; -import java.util.zip.GZIPOutputStream; -import javax.servlet.http.HttpServletRequest; +import java.util.Objects; import javax.servlet.http.HttpServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -94,6 +100,8 @@ public class TrackerEventsExportController { private final EventFieldsParamMapper eventsMapper; + private final ObjectMapper objectMapper; + @GetMapping(produces = APPLICATION_JSON_VALUE) public PagingWrapper getEvents( TrackerEventCriteria eventCriteria, @@ -123,12 +131,60 @@ public PagingWrapper getEvents( return pagingWrapper.withInstances(objectNodes); } - @GetMapping(produces = {CONTENT_TYPE_CSV, CONTENT_TYPE_CSV_GZIP, CONTENT_TYPE_TEXT_CSV}) - public void getCsvEvents( + @GetMapping(produces = CONTENT_TYPE_JSON_GZIP) + void getEventsAsGzip(TrackerEventCriteria eventCriteria, HttpServletResponse response) + throws IOException { + EventQueryParams eventQueryParams = requestToSearchParamsMapper.map(eventCriteria); + + if (areAllEnrollmentsInvalid(eventCriteria, eventQueryParams)) { + return; + } + + Events events = eventService.getEvents(eventQueryParams); + + String attachment = getAttachmentOrDefault(eventCriteria.getAttachment(), "json", "gz"); + + response.addHeader( + ContextUtils.HEADER_CONTENT_DISPOSITION, getContentDispositionHeaderValue(attachment)); + response.addHeader(HEADER_CONTENT_TRANSFER_ENCODING, "binary"); + response.setContentType(CONTENT_TYPE_JSON_GZIP); + + writeGzip( + response.getOutputStream(), + EVENTS_MAPPER.fromCollection(events.getEvents()), + objectMapper.writer()); + } + + @GetMapping(produces = CONTENT_TYPE_JSON_ZIP) + void getEventsAsZip(TrackerEventCriteria eventCriteria, HttpServletResponse response) + throws IOException { + EventQueryParams eventQueryParams = requestToSearchParamsMapper.map(eventCriteria); + + if (areAllEnrollmentsInvalid(eventCriteria, eventQueryParams)) { + return; + } + + Events events = eventService.getEvents(eventQueryParams); + + String attachment = getAttachmentOrDefault(eventCriteria.getAttachment(), "json", "zip"); + + response.addHeader( + ContextUtils.HEADER_CONTENT_DISPOSITION, getContentDispositionHeaderValue(attachment)); + response.addHeader(HEADER_CONTENT_TRANSFER_ENCODING, "binary"); + response.setContentType(CONTENT_TYPE_JSON_ZIP); + + writeZip( + response.getOutputStream(), + EVENTS_MAPPER.fromCollection(events.getEvents()), + objectMapper.writer(), + attachment); + } + + @GetMapping(produces = {CONTENT_TYPE_CSV, CONTENT_TYPE_TEXT_CSV}) + void getEventsAsCsv( TrackerEventCriteria eventCriteria, HttpServletResponse response, - @RequestParam(required = false, defaultValue = "false") boolean skipHeader, - HttpServletRequest request) + @RequestParam(required = false, defaultValue = "false") boolean skipHeader) throws IOException { EventQueryParams eventQueryParams = requestToSearchParamsMapper.map(eventCriteria); @@ -138,21 +194,82 @@ public void getCsvEvents( Events events = eventService.getEvents(eventQueryParams); + String attachment = getAttachmentOrDefault(eventCriteria.getAttachment(), "csv"); + OutputStream outputStream = response.getOutputStream(); response.setContentType(CONTENT_TYPE_CSV); - response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"events.csv\""); - - if (ContextUtils.isAcceptCsvGzip(request)) { - response.addHeader(ContextUtils.HEADER_CONTENT_TRANSFER_ENCODING, "binary"); - outputStream = new GZIPOutputStream(outputStream); - response.setContentType(CONTENT_TYPE_CSV_GZIP); - response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"events.csv.gz\""); - } + response.setHeader( + HttpHeaders.CONTENT_DISPOSITION, getContentDispositionHeaderValue(attachment)); csvEventService.writeEvents( outputStream, EVENTS_MAPPER.fromCollection(events.getEvents()), !skipHeader); } + @GetMapping(produces = {CONTENT_TYPE_CSV_GZIP}) + void getEventsAsCsvGZip( + TrackerEventCriteria eventCriteria, + HttpServletResponse response, + @RequestParam(required = false, defaultValue = "false") boolean skipHeader) + throws IOException { + EventQueryParams eventQueryParams = requestToSearchParamsMapper.map(eventCriteria); + + if (areAllEnrollmentsInvalid(eventCriteria, eventQueryParams)) { + return; + } + + Events events = eventService.getEvents(eventQueryParams); + + String attachment = getAttachmentOrDefault(eventCriteria.getAttachment(), "csv", "gz"); + + response.addHeader(ContextUtils.HEADER_CONTENT_TRANSFER_ENCODING, "binary"); + response.setContentType(CONTENT_TYPE_CSV_GZIP); + response.addHeader( + ContextUtils.HEADER_CONTENT_DISPOSITION, getContentDispositionHeaderValue(attachment)); + + csvEventService.writeGzip( + response.getOutputStream(), EVENTS_MAPPER.fromCollection(events.getEvents()), !skipHeader); + } + + @GetMapping(produces = {CONTENT_TYPE_CSV_ZIP}) + void getEventsAsCsvZip( + TrackerEventCriteria eventCriteria, + HttpServletResponse response, + @RequestParam(required = false, defaultValue = "false") boolean skipHeader) + throws IOException { + EventQueryParams eventQueryParams = requestToSearchParamsMapper.map(eventCriteria); + + if (areAllEnrollmentsInvalid(eventCriteria, eventQueryParams)) { + return; + } + + Events events = eventService.getEvents(eventQueryParams); + + String attachment = getAttachmentOrDefault(eventCriteria.getAttachment(), "csv", "zip"); + + response.addHeader(ContextUtils.HEADER_CONTENT_TRANSFER_ENCODING, "binary"); + response.setContentType(CONTENT_TYPE_CSV_ZIP); + response.addHeader( + ContextUtils.HEADER_CONTENT_DISPOSITION, getContentDispositionHeaderValue(attachment)); + + csvEventService.writeZip( + response.getOutputStream(), + EVENTS_MAPPER.fromCollection(events.getEvents()), + !skipHeader, + attachment); + } + + private String getAttachmentOrDefault(String filename, String type, String compression) { + return Objects.toString(filename, String.join(".", EVENTS, type, compression)); + } + + private String getAttachmentOrDefault(String filename, String type) { + return Objects.toString(filename, String.join(".", EVENTS, type)); + } + + public String getContentDispositionHeaderValue(String filename) { + return "attachment; filename=" + filename; + } + private boolean areAllEnrollmentsInvalid( TrackerEventCriteria eventCriteria, EventQueryParams eventQueryParams) { return !CollectionUtils.isEmpty(eventCriteria.getEnrollments()) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/ContextUtils.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/ContextUtils.java index a60c217e12b2..eed4b4ce71d3 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/ContextUtils.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/ContextUtils.java @@ -57,7 +57,8 @@ public class ContextUtils { public static final String CONTENT_TYPE_PDF = "application/pdf"; - public static final String CONTENT_TYPE_ZIP = "application/zip"; + public static final String CONTENT_TYPE_JSON_ZIP = "application/json+zip"; + public static final String CONTENT_TYPE_JSON_GZIP = "application/json+gzip"; public static final String CONTENT_TYPE_GZIP = "application/gzip"; @@ -78,6 +79,7 @@ public class ContextUtils { public static final String CONTENT_TYPE_TEXT_CSV = "text/csv"; public static final String CONTENT_TYPE_CSV_GZIP = "application/csv+gzip"; + public static final String CONTENT_TYPE_CSV_ZIP = "application/csv+zip"; public static final String CONTENT_TYPE_PNG = "image/png"; diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/csv/TrackerCsvEventServiceTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/csv/TrackerCsvEventServiceTest.java index 196c4ac7f433..9ab64dba68dd 100644 --- a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/csv/TrackerCsvEventServiceTest.java +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/csv/TrackerCsvEventServiceTest.java @@ -29,10 +29,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.common.io.Files; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -40,6 +42,9 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import org.hisp.dhis.event.EventStatus; import org.hisp.dhis.tracker.domain.DataValue; import org.hisp.dhis.tracker.domain.Event; @@ -194,4 +199,65 @@ void testReadEventsFromFileWithoutHeader() throws IOException, ParseException { assertFalse(dv.isProvidedElsewhere()); }); } + + @Test + void zipFileAndMatchCsvEventDataValuesWhenCreateZipFileFromEventList() + throws IOException, ParseException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + File event = new File("src/test/resources/controller/tracker/csv/completeEvent.csv"); + InputStream inputStream = Files.asByteSource(event).openStream(); + + List events = service.readEvents(inputStream, false); + + service.writeZip(outputStream, events, false, "file.json.zip"); + + ZipInputStream zipInputStream = + new ZipInputStream(new ByteArrayInputStream(outputStream.toByteArray())); + var buff = new byte[1024]; + + ZipEntry zipEntry = zipInputStream.getNextEntry(); + + assertNotNull(zipEntry, "Events Zip file has no entry"); + assertEquals("file.json.zip", zipEntry.getName(), "Events Zip file has a wrong name"); + + var csvStream = new ByteArrayOutputStream(); + int l; + while ((l = zipInputStream.read(buff)) > 0) { + csvStream.write(buff, 0, l); + } + + assertEquals( + "eventId,COMPLETED,programId,programStageId,enrollmentId,orgUnitId,,2020-02-26T23:01:00Z,2020-02-26T23:02:00Z,,,,false,false,2020-02-26T23:03:00Z,,2020-02-26T23:05:00Z,,admin,2020-02-26T23:07:00Z,,,,,dataElement,value,admin,false,,2020-02-26T23:08:00Z,2020-02-26T23:09:00Z\n", + csvStream.toString(), + "The event does not match or not exists in the Zip File."); + } + + @Test + void gzipFileAndMatchCsvEventDataValuesWhenCreateGZipFileFromEventList() + throws IOException, ParseException { + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + File event = new File("src/test/resources/controller/tracker/csv/completeEvent.csv"); + InputStream inputStream = Files.asByteSource(event).openStream(); + + List events = service.readEvents(inputStream, false); + service.writeGzip(outputStream, events, false); + + GZIPInputStream gzipInputStream = + new GZIPInputStream(new ByteArrayInputStream(outputStream.toByteArray())); + var buff = new byte[1024]; + + var csvStream = new ByteArrayOutputStream(); + int l; + while ((l = gzipInputStream.read(buff)) > 0) { + csvStream.write(buff, 0, l); + } + + assertEquals( + "eventId,COMPLETED,programId,programStageId,enrollmentId,orgUnitId,,2020-02-26T23:01:00Z,2020-02-26T23:02:00Z,,,,false,false,2020-02-26T23:03:00Z,,2020-02-26T23:05:00Z,,admin,2020-02-26T23:07:00Z,,,,,dataElement,value,admin,false,,2020-02-26T23:08:00Z,2020-02-26T23:09:00Z\n", + csvStream.toString(), + "The event does not match or not exists in the GZip File."); + } } diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/CompressionUtilTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/CompressionUtilTest.java new file mode 100644 index 000000000000..9c9e64c1958b --- /dev/null +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/CompressionUtilTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2004-2022, 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: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 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. + * Neither the name of the HISP project 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.export; + +import static org.hisp.dhis.webapi.controller.tracker.export.CompressionUtil.writeGzip; +import static org.hisp.dhis.webapi.controller.tracker.export.CompressionUtil.writeZip; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.tracker.domain.Event; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CompressionUtilTest { + + private static final Event FIRST_EVENT = new Event(); + private static final Event SECOND_EVENT = new Event(); + @InjectMocks private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + FIRST_EVENT.setEvent(CodeGenerator.generateUid()); + SECOND_EVENT.setEvent(CodeGenerator.generateUid()); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + @Test + void shouldUnzipFileAndMatchEventsWhenCreateZipFileFromEventList() throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + List eventToZip = getEvents(); + + writeZip(outputStream, eventToZip, objectMapper.writer(), "file.json.zip"); + + ZipInputStream zipInputStream = + new ZipInputStream(new ByteArrayInputStream(outputStream.toByteArray())); + var buff = new byte[1024]; + List eventsFromZip; + + ZipEntry zipEntry = zipInputStream.getNextEntry(); + + assertNotNull(zipEntry, "Events Zip file has no entry"); + assertEquals("file.json.zip", zipEntry.getName(), "Events Zip file has a wrong name"); + + var byteArrayOutputStream = new ByteArrayOutputStream(); + int l; + while ((l = zipInputStream.read(buff)) > 0) { + byteArrayOutputStream.write(buff, 0, l); + } + eventsFromZip = + objectMapper.readValue(byteArrayOutputStream.toString(), new TypeReference<>() {}); + + assertNull(zipInputStream.getNextEntry()); // assert only one file is created + assertEquals(eventToZip.size(), eventsFromZip.size()); + assertEquals( + FIRST_EVENT, + eventsFromZip.stream() + .filter(e -> e.getEvent().equals(FIRST_EVENT.getEvent())) + .findAny() + .orElse(null), + "The event does not match or not exists in the Zip File."); + assertEquals( + SECOND_EVENT, + eventsFromZip.stream() + .filter(e -> e.getEvent().equals(SECOND_EVENT.getEvent())) + .findAny() + .orElse(null), + "The event does not match or not exists in the Zip File."); + } + + @Test + void shouldGUnzipFileAndMatchEventsWhenCreateGZipFileFromEventList() throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + List eventToGZip = getEvents(); + + writeGzip(outputStream, eventToGZip, objectMapper.writer()); + + GZIPInputStream gzipInputStream = + new GZIPInputStream(new ByteArrayInputStream(outputStream.toByteArray())); + var buff = new byte[1024]; + List eventsFromGZip; + + var byteArrayOutputStream = new ByteArrayOutputStream(); + int l; + while ((l = gzipInputStream.read(buff)) > 0) { + byteArrayOutputStream.write(buff, 0, l); + } + eventsFromGZip = + objectMapper.readValue(byteArrayOutputStream.toString(), new TypeReference<>() {}); + + assertEquals(eventToGZip.size(), eventsFromGZip.size()); + assertEquals( + FIRST_EVENT, + eventToGZip.stream() + .filter(e -> e.getEvent().equals(FIRST_EVENT.getEvent())) + .findAny() + .orElse(null), + "The event does not match or not exists in the GZip File."); + assertEquals( + SECOND_EVENT, + eventToGZip.stream() + .filter(e -> e.getEvent().equals(SECOND_EVENT.getEvent())) + .findAny() + .orElse(null), + "The event does not match or not exists in the GZip File."); + } + + List getEvents() { + return List.of(FIRST_EVENT, SECOND_EVENT); + } +}