From 8aa828864ebc86f031b339994614522e40e39805 Mon Sep 17 00:00:00 2001 From: Leonardo Gonzales Date: Wed, 22 Jul 2026 13:36:30 +0100 Subject: [PATCH 1/4] feat: Add saveFileResource --- src/main/java/org/hisp/dhis/BaseDhis2.java | 13 +++++ src/main/java/org/hisp/dhis/Dhis2.java | 39 +++++++++++++ .../fileresource/FileResourceReport.java | 45 +++++++++++++++ .../fileresource/FileResourceResponse.java | 48 ++++++++++++++++ .../org/hisp/dhis/FileResourceApiTest.java | 55 +++++++++++++++++++ src/test/java/org/hisp/dhis/TestFixture.java | 2 +- 6 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/hisp/dhis/response/fileresource/FileResourceReport.java create mode 100644 src/main/java/org/hisp/dhis/response/fileresource/FileResourceResponse.java diff --git a/src/main/java/org/hisp/dhis/BaseDhis2.java b/src/main/java/org/hisp/dhis/BaseDhis2.java index d6ef3585..02fca06d 100644 --- a/src/main/java/org/hisp/dhis/BaseDhis2.java +++ b/src/main/java/org/hisp/dhis/BaseDhis2.java @@ -1079,6 +1079,19 @@ protected HttpPost getPostRequest(URI url) { return request; } + /** + * Returns a HTTP post request with a multipart entity for the given URL, e.g. for file uploads. + * + * @param url the {@link URI}. + * @param entity the multipart {@link HttpEntity}. + * @return a {@link HttpPost} request. + */ + protected HttpPost getMultipartPostRequest(URI url, HttpEntity entity) { + HttpPost request = withAuth(new HttpPost(url)); + request.setEntity(entity); + return request; + } + /** * Retrieves an object using HTTP GET. * diff --git a/src/main/java/org/hisp/dhis/Dhis2.java b/src/main/java/org/hisp/dhis/Dhis2.java index 370de5d6..d4e95daf 100644 --- a/src/main/java/org/hisp/dhis/Dhis2.java +++ b/src/main/java/org/hisp/dhis/Dhis2.java @@ -70,7 +70,9 @@ import org.apache.hc.client5.http.HttpResponseException; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.io.entity.FileEntity; import org.apache.hc.core5.http.io.entity.InputStreamEntity; import org.apache.hc.core5.http.io.entity.StringEntity; @@ -183,6 +185,7 @@ import org.hisp.dhis.response.data.ImportSummaryResponse; import org.hisp.dhis.response.datavalueset.DataValueSetResponse; import org.hisp.dhis.response.event.EventResponse; +import org.hisp.dhis.response.fileresource.FileResourceResponse; import org.hisp.dhis.response.job.JobCategory; import org.hisp.dhis.response.job.JobInfoResponse; import org.hisp.dhis.response.job.JobNotification; @@ -3919,6 +3922,42 @@ public List getFileResources(Query query) { .getFileResources(); } + /** + * Creates a {@link FileResource} by uploading the given file. + * + * @param file the {@link File} to upload. + * @return a {@link FileResourceResponse}. + * @throws Dhis2ClientException if the request was invalid. + */ + public FileResourceResponse saveFileResource(File file) { + HttpEntity entity = MultipartEntityBuilder.create().addBinaryBody("file", file).build(); + + URI url = HttpUtils.build(config.getResolvedUriBuilder().appendPath(PATH_FILE_RESOURCES)); + + return executeRequest(getMultipartPostRequest(url, entity), FileResourceResponse.class); + } + + /** + * Creates a {@link FileResource} by uploading the content of the given {@link InputStream}. + * + * @param fileName the name of the file. + * @param contentType the content type of the file, e.g. "image/png". + * @param input the {@link InputStream} of the file content. + * @return a {@link FileResourceResponse}. + * @throws Dhis2ClientException if the request was invalid. + */ + public FileResourceResponse saveFileResource( + String fileName, String contentType, InputStream input) { + HttpEntity entity = + MultipartEntityBuilder.create() + .addBinaryBody("file", input, ContentType.parse(contentType), fileName) + .build(); + + URI url = HttpUtils.build(config.getResolvedUriBuilder().appendPath(PATH_FILE_RESOURCES)); + + return executeRequest(getMultipartPostRequest(url, entity), FileResourceResponse.class); + } + /** * Downloads file content from the given URL. * diff --git a/src/main/java/org/hisp/dhis/response/fileresource/FileResourceReport.java b/src/main/java/org/hisp/dhis/response/fileresource/FileResourceReport.java new file mode 100644 index 00000000..b61effec --- /dev/null +++ b/src/main/java/org/hisp/dhis/response/fileresource/FileResourceReport.java @@ -0,0 +1,45 @@ +/* + * 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: + * 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.response.fileresource; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.hisp.dhis.model.FileResource; + +@Getter +@Setter +@ToString +@NoArgsConstructor +public class FileResourceReport { + @JsonProperty private String responseType; + + @JsonProperty private FileResource fileResource; +} diff --git a/src/main/java/org/hisp/dhis/response/fileresource/FileResourceResponse.java b/src/main/java/org/hisp/dhis/response/fileresource/FileResourceResponse.java new file mode 100644 index 00000000..d5d49af0 --- /dev/null +++ b/src/main/java/org/hisp/dhis/response/fileresource/FileResourceResponse.java @@ -0,0 +1,48 @@ +/* + * 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: + * 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.response.fileresource; + +import static org.hisp.dhis.util.TextUtils.newToStringBuilder; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hisp.dhis.response.Response; + +@Getter +@Setter +@NoArgsConstructor +public class FileResourceResponse extends Response { + @JsonProperty protected FileResourceReport response; + + @Override + public String toString() { + return newToStringBuilder(this, super.toString()).append("response", response).toString(); + } +} diff --git a/src/test/java/org/hisp/dhis/FileResourceApiTest.java b/src/test/java/org/hisp/dhis/FileResourceApiTest.java index 8277197c..fb3b83de 100644 --- a/src/test/java/org/hisp/dhis/FileResourceApiTest.java +++ b/src/test/java/org/hisp/dhis/FileResourceApiTest.java @@ -32,9 +32,17 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.List; import org.hisp.dhis.model.FileResource; import org.hisp.dhis.query.Query; +import org.hisp.dhis.response.fileresource.FileResourceReport; +import org.hisp.dhis.response.fileresource.FileResourceResponse; import org.hisp.dhis.support.TestTags; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -101,4 +109,51 @@ void testGetEventFile() { assertNotNull(data); assertTrue(data.length > 0); } + + @Test + void testSaveFileResource() throws IOException { + Dhis2 dhis2 = new Dhis2(TestFixture.DEFAULT_CONFIG); + + File file = File.createTempFile("dhis2-java-client-test", ".txt"); + file.deleteOnExit(); + Files.writeString(file.toPath(), "Dhis2 Java client file resource test content"); + + FileResourceResponse response = dhis2.saveFileResource(file); + + assertNotNull(response); + assertTrue(response.isStatusOk()); + + FileResourceReport report = response.getResponse(); + + assertNotNull(report); + + FileResource fileResource = report.getFileResource(); + + assertNotNull(fileResource); + assertNotBlank(fileResource.getId()); + } + + @Test + void testSaveFileResourceFromInputStream() { + Dhis2 dhis2 = new Dhis2(TestFixture.DEFAULT_CONFIG); + + InputStream input = + new ByteArrayInputStream( + "Dhis2 Java client file resource test content".getBytes(StandardCharsets.UTF_8)); + + FileResourceResponse response = + dhis2.saveFileResource("dhis2-java-client-test.txt", "text/plain", input); + + assertNotNull(response); + assertTrue(response.isStatusOk()); + + FileResourceReport report = response.getResponse(); + + assertNotNull(report); + + FileResource fileResource = report.getFileResource(); + + assertNotNull(fileResource); + assertNotBlank(fileResource.getId()); + } } diff --git a/src/test/java/org/hisp/dhis/TestFixture.java b/src/test/java/org/hisp/dhis/TestFixture.java index dc910f52..40c83f89 100644 --- a/src/test/java/org/hisp/dhis/TestFixture.java +++ b/src/test/java/org/hisp/dhis/TestFixture.java @@ -38,7 +38,7 @@ public final class TestFixture { public static final String DEV_URL = "https://play.im.dhis2.org/dev"; - public static final String V41_URL = "https://play.im.dhis2.org/stable-2-41-8-1"; + public static final String V41_URL = "https://play.im.dhis2.org/stable-2-41-9"; public static final String V42_URL = "https://play.im.dhis2.org/stable-2-42-4-1"; From 7c0bea9e1eff4e54806c96eb91ed4d748ac52d77 Mon Sep 17 00:00:00 2001 From: Leonardo Gonzales Date: Mon, 27 Jul 2026 16:33:14 +0100 Subject: [PATCH 2/4] chore: Update getEventFile path --- src/main/java/org/hisp/dhis/Dhis2.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/hisp/dhis/Dhis2.java b/src/main/java/org/hisp/dhis/Dhis2.java index d4e95daf..ab294f09 100644 --- a/src/main/java/org/hisp/dhis/Dhis2.java +++ b/src/main/java/org/hisp/dhis/Dhis2.java @@ -4068,10 +4068,12 @@ public byte[] getEventFile(String eventUid, String dataElementUid) { HttpUtils.build( config .getResolvedUriBuilder() + .appendPath(PATH_TRACKER) .appendPath(PATH_EVENTS) - .appendPath("files") - .addParameter("eventUid", eventUid) - .addParameter("dataElementUid", dataElementUid)); + .appendPath(eventUid) + .appendPath("dataValues") + .appendPath(dataElementUid) + .appendPath("file")); return downloadFile(uri, "Failed to download event file"); } From 50c0f85d412ba878a277d999dc50a5ffbc20be5f Mon Sep 17 00:00:00 2001 From: Leonardo Gonzales Date: Mon, 27 Jul 2026 17:50:26 +0100 Subject: [PATCH 3/4] chore: Bump version to 2.6.10 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 665dbb4f..3211255a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.hisp dhis2-java-client - 2.6.9 + 2.6.10 jar DHIS 2 API client for Java From 0064b1016dcd4a357d090f1158fc203266923bb8 Mon Sep 17 00:00:00 2001 From: Leonardo Gonzales Date: Mon, 27 Jul 2026 17:51:42 +0100 Subject: [PATCH 4/4] chore: Improve addToCollection logic --- src/main/java/org/hisp/dhis/BaseDhis2.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/java/org/hisp/dhis/BaseDhis2.java b/src/main/java/org/hisp/dhis/BaseDhis2.java index 02fca06d..362dcc2a 100644 --- a/src/main/java/org/hisp/dhis/BaseDhis2.java +++ b/src/main/java/org/hisp/dhis/BaseDhis2.java @@ -1460,16 +1460,7 @@ protected Response addToCollection(String path, String id, String collection, St .appendPath(collection) .appendPath(item)); - Response response = executeRequest(new HttpPost(url)); - - Status status = - response != null - && response.getHttpStatus() != null - && response.getHttpStatus().is2xxSuccessful() - ? Status.OK - : Status.ERROR; - - return new Response(status, response.getHttpStatusCode(), response.getMessage()); + return executeRequest(new HttpPost(url)); } /**