diff --git a/fineract-client/src/main/java/org/apache/fineract/client/services/DocumentsApiFixed.java b/fineract-client/src/main/java/org/apache/fineract/client/services/DocumentsApiFixed.java new file mode 100644 index 000000000..29e631c7d --- /dev/null +++ b/fineract-client/src/main/java/org/apache/fineract/client/services/DocumentsApiFixed.java @@ -0,0 +1,147 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.client.services; + +import java.util.List; +import okhttp3.ResponseBody; +import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse; +import org.apache.fineract.client.models.GetEntityTypeEntityIdDocumentsResponse; +import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse; +import org.apache.fineract.client.models.PutEntityTypeEntityIdDocumentsResponse; +import retrofit2.Call; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.PUT; + +// This class was originally generated by OpenAPI Generator (https://openapi-generator.tech), +// but then had to be manually edited by Michael Vorburger.ch to manually fix https://issues.apache.org/jira/browse/FINERACT-1227. +// If we could fix our OpenAPI / Swagger YAML generation from the JAX RS and OpenAPI annotation to have the correct notation for binary document files and images, then this could be removed again. +public interface DocumentsApiFixed { + + /** + * Create a Document Note: A document is created using a Multi-part form upload Body Parts name : Name or summary of + * the document description : Description of the document file : The file to be uploaded Mandatory Fields : file and + * description + * + * @param entityType + * entityType (required) + * @param entityId + * entityId (required) + * @param file + * (required) + * @param name + * name (optional) + * @param description + * description (optional) + * @return Call<PostEntityTypeEntityIdDocumentsResponse> + */ + @retrofit2.http.Multipart + @POST("{entityType}/{entityId}/documents") + Call createDocument(@retrofit2.http.Path("entityType") String entityType, + @retrofit2.http.Path("entityId") Long entityId, @retrofit2.http.Part okhttp3.MultipartBody.Part file, + @retrofit2.http.Part("name") String name, @retrofit2.http.Part("description") String description); + + /** + * Remove a Document + * + * @param entityType + * entityType (required) + * @param entityId + * entityId (required) + * @param documentId + * documentId (required) + * @return Call<DeleteEntityTypeEntityIdDocumentsResponse> + */ + @DELETE("{entityType}/{entityId}/documents/{documentId}") + Call deleteDocument(@retrofit2.http.Path("entityType") String entityType, + @retrofit2.http.Path("entityId") Long entityId, @retrofit2.http.Path("documentId") Long documentId); + + /** + * Retrieve Binary File associated with Document Request used to download the file associated with the document + * Example Requests: clients/1/documents/1/attachment loans/1/documents/1/attachment + * + * @param entityType + * entityType (required) + * @param entityId + * entityId (required) + * @param documentId + * documentId (required) + * @return Call<Void> + */ + @GET("{entityType}/{entityId}/documents/{documentId}/attachment") + Call downloadFile(@retrofit2.http.Path("entityType") String entityType, @retrofit2.http.Path("entityId") Long entityId, + @retrofit2.http.Path("documentId") Long documentId); + + /** + * Retrieve a Document Example Requests: clients/1/documents/1 loans/1/documents/1 + * client_identifiers/1/documents/1?fields=name,description + * + * @param entityType + * entityType (required) + * @param entityId + * entityId (required) + * @param documentId + * documentId (required) + * @return Call<GetEntityTypeEntityIdDocumentsResponse> + */ + @GET("{entityType}/{entityId}/documents/{documentId}") + Call getDocument(@retrofit2.http.Path("entityType") String entityType, + @retrofit2.http.Path("entityId") Long entityId, @retrofit2.http.Path("documentId") Long documentId); + + /** + * List documents Example Requests: clients/1/documents client_identifiers/1/documents + * loans/1/documents?fields=name,description + * + * @param entityType + * entityType (required) + * @param entityId + * entityId (required) + * @return Call<List<GetEntityTypeEntityIdDocumentsResponse>> + */ + @GET("{entityType}/{entityId}/documents") + Call> retrieveAllDocuments(@retrofit2.http.Path("entityType") String entityType, + @retrofit2.http.Path("entityId") Long entityId); + + /** + * Update a Document Note: A document is updated using a Multi-part form upload Body Parts name Name or summary of + * the document description Description of the document file The file to be uploaded + * + * @param entityType + * entityType (required) + * @param entityId + * entityId (required) + * @param documentId + * documentId (required) + * @param file + * (optional) + * @param name + * name (optional) + * @param description + * description (optional) + * @return Call<PutEntityTypeEntityIdDocumentsResponse> + */ + @retrofit2.http.Multipart + @PUT("{entityType}/{entityId}/documents/{documentId}") + Call updateDocument(@retrofit2.http.Path("entityType") String entityType, + @retrofit2.http.Path("entityId") Long entityId, @retrofit2.http.Path("documentId") Long documentId, + @retrofit2.http.Part okhttp3.MultipartBody.Part file, @retrofit2.http.Part("name") String name, + @retrofit2.http.Part("description") String description); + +} diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java index a7e85395a..48908bc3a 100644 --- a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java +++ b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java @@ -56,7 +56,7 @@ import org.apache.fineract.client.services.CodesApi; import org.apache.fineract.client.services.CurrencyApi; import org.apache.fineract.client.services.DataTablesApi; import org.apache.fineract.client.services.DefaultApi; -import org.apache.fineract.client.services.DocumentsApi; +import org.apache.fineract.client.services.DocumentsApiFixed; import org.apache.fineract.client.services.EntityDataTableApi; import org.apache.fineract.client.services.EntityFieldConfigurationApi; import org.apache.fineract.client.services.ExternalServicesApi; @@ -181,7 +181,7 @@ public final class FineractClient { public final CurrencyApi currencies; public final DataTablesApi dataTables; public final @Deprecated DefaultApi legacy; // TODO FINERACT-1222 - public final DocumentsApi documents; + public final DocumentsApiFixed documents; public final EntityDataTableApi entityDatatableChecks; public final EntityFieldConfigurationApi entityFieldConfigurations; public final ExternalServicesApi externalServices; @@ -288,7 +288,7 @@ public final class FineractClient { currencies = retrofit.create(CurrencyApi.class); dataTables = retrofit.create(DataTablesApi.class); legacy = retrofit.create(DefaultApi.class); - documents = retrofit.create(DocumentsApi.class); + documents = retrofit.create(DocumentsApiFixed.class); entityDatatableChecks = retrofit.create(EntityDataTableApi.class); entityFieldConfigurations = retrofit.create(EntityFieldConfigurationApi.class); externalServices = retrofit.create(ExternalServicesApi.class); diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/Parts.java b/fineract-client/src/main/java/org/apache/fineract/client/util/Parts.java new file mode 100644 index 000000000..d54fac568 --- /dev/null +++ b/fineract-client/src/main/java/org/apache/fineract/client/util/Parts.java @@ -0,0 +1,86 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.client.util; + +import java.io.File; +import okhttp3.MediaType; +import okhttp3.MultipartBody.Part; +import okhttp3.RequestBody; + +/** + * Convenience Factory for {@link Part} (including {@link RequestBody}). + * + * @author Michael Vorburger.ch + */ +public final class Parts { + + private Parts() {} + + public static Part fromFile(File file) { + RequestBody rb = RequestBody.create(file, getMediaType(file.getName())); + return Part.createFormData("file", file.getName(), rb); + } + + public static Part fromFile(String fileName, byte[] bytes) { + RequestBody rb = RequestBody.create(bytes, getMediaType(fileName)); + return Part.createFormData("file", fileName, rb); + } + + // TODO this logic should be on the Server, not have to be done by the client... + // There actually does seem to be some code related to MIME type guessing in Fineract, but test shows it doesn't + // work :( + private static MediaType getMediaType(String fileName) { + int dotPos = fileName.lastIndexOf('.'); + if (dotPos == -1) { + return null; + } + String ext = fileName.substring(dotPos); + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + switch (ext) { + case "jpg": + case "jpeg": + return MediaType.get("image/jpeg"); + case "png": + return MediaType.get("image/png"); + case "tif": + case "tiff": + return MediaType.get("image/tiff"); + case "gif": + return MediaType.get("image/gif"); + case "pdf": + return MediaType.get("application/pdf"); + case "docx": + return MediaType.get("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + case "doc": + return MediaType.get("application/msword"); + case "xlsx": + return MediaType.get("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + case "xls": + return MediaType.get("application/vnd.ms-excel"); + case "odt": + return MediaType.get("application/vnd.oasis.opendocument.text"); + case "ods": + return MediaType.get("application/vnd.oasis.opendocument.spreadsheet"); + case "txt": + return MediaType.get("text/plain"); + default: + return null; + } + } +} diff --git a/fineract-client/src/test/java/org/apache/fineract/integrationtests/newstyle/ClientTest.java b/fineract-client/src/test/java/org/apache/fineract/integrationtests/newstyle/ClientTest.java new file mode 100644 index 000000000..af6b66c2d --- /dev/null +++ b/fineract-client/src/test/java/org/apache/fineract/integrationtests/newstyle/ClientTest.java @@ -0,0 +1,72 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.newstyle; + +import java.util.Optional; +import org.apache.fineract.client.models.GetClientsResponse; +import org.apache.fineract.client.models.PostClientsRequest; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +/** + * Integration Test for /clients API. + * + * @author Michael Vorburger.ch + */ +public class ClientTest extends IntegrationTest { + + @Test + @Order(1) + void createOne() { + assertThat(create()).isGreaterThan(0); + } + + @Test + @Order(2) + void retrieveOneExisting() { + assertThat(retrieveOne()).isPresent(); + } + + // The following are not tests, but helpful utilities for other tests + + public Long getClientId() { + return retrieveOne().orElseGet(this::create); + } + + Long create() { + // NB officeId(1) always exists (Head Office) + // TODO rm long cast, see https://issues.apache.org/jira/browse/FINERACT-1230 + // TODO activationDate() why String? https://issues.apache.org/jira/browse/FINERACT-1232 + // TODO why dateFormat and locale required even when no activationDate?! + // https://issues.apache.org/jira/browse/FINERACT-1233 + return (long) ok(fineract().clients + .create5(new PostClientsRequest().officeId(1).fullname("TestClient").dateFormat(dateFormat()).locale("en_US"))) + .getClientId(); + } + + Optional retrieveOne() { + GetClientsResponse clients = ok( + fineract().clients.retrieveAll20(null, null, null, null, null, null, null, null, 0, 1, null, null, false)); + if (clients.getTotalFilteredRecords() > 0) { + // TODO rm long cast, see https://issues.apache.org/jira/browse/FINERACT-1230 + return Optional.of((long) clients.getPageItems().get(0).getId()); + } + return Optional.empty(); + } +} diff --git a/fineract-client/src/test/java/org/apache/fineract/integrationtests/newstyle/DocumentTest.java b/fineract-client/src/test/java/org/apache/fineract/integrationtests/newstyle/DocumentTest.java new file mode 100644 index 000000000..991ad1395 --- /dev/null +++ b/fineract-client/src/test/java/org/apache/fineract/integrationtests/newstyle/DocumentTest.java @@ -0,0 +1,115 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.newstyle; + +import java.io.File; +import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.MultipartBody.Part; +import okhttp3.ResponseBody; +import org.apache.fineract.client.models.GetEntityTypeEntityIdDocumentsResponse; +import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse; +import org.apache.fineract.client.util.Parts; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +/** + * Integration Test for /documents API. + * + * @author Michael Vorburger.ch + */ +public class DocumentTest extends IntegrationTest { + + final File testFile = new File(getClass().getResource("/michael.vorburger-crepes.jpg").getFile()); + + Long clientId = new ClientTest().getClientId(); + Long documentId; + + @Test + @Order(1) + void retrieveAllDocuments() throws IOException { + assertThat(ok(fineract().documents.retrieveAllDocuments("clients", clientId))).isNotNull(); + } + + @Test + @Order(2) + void createDocument() throws IOException { + String name = "Test"; + Part part = Parts.fromFile(testFile); + String description = null; + // TODO used var in tests when moved from fineract-client (Java 8 only) to new module + PostEntityTypeEntityIdDocumentsResponse response = ok( + fineract().documents.createDocument("clients", clientId, part, name, description)); + assertThat(response.getResourceId()).isNotNull(); + assertThat(response.getResourceIdentifier()).isNotEmpty(); + documentId = response.getResourceId(); + } + + @Test + @Order(3) + void getDocument() throws IOException { + GetEntityTypeEntityIdDocumentsResponse doc = ok(fineract().documents.getDocument("clients", clientId, documentId)); + assertThat(doc.getName()).isEqualTo("Test"); + assertThat(doc.getFileName()).isEqualTo(testFile.getName()); + assertThat(doc.getDescription()).isNull(); + assertThat(doc.getId()).isEqualTo(documentId); + assertThat(doc.getParentEntityType()).isEqualTo("clients"); + assertThat(doc.getParentEntityId()).isEqualTo(clientId); + // TODO huh?! It's more than uploaded file; seems like a bug - it's including create body, not just file size + assertThat(doc.getSize()).isEqualTo(testFile.length() + 385); + // TODO huh?! MIME is always text/plain instead of image/jpeg... :( + assertThat(doc.getType()).isEqualTo("text/plain"); + // TODO doc.getStorageType() shouldn't be exposed by the API?! + } + + @Test + @Order(4) + void downloadFile() throws IOException { + ResponseBody r = ok(fineract().documents.downloadFile("clients", clientId, documentId)); + assertThat(r.contentType()).isEqualTo(MediaType.get("text/plain")); // TODO wrong, bug; needs to be "image/jpeg" + // (as above) + assertThat(r.bytes().length).isEqualTo(testFile.length()); + // NOK: assertThat(r.contentLength()).isEqualTo(testFile.length()); + } + + @Test + @Order(10) + void updateDocument() throws IOException { + String newName = "Test changed name"; + String newDescription = getClass().getName(); + ok(fineract().documents.updateDocument("clients", clientId, documentId, null, newName, newDescription)); + + GetEntityTypeEntityIdDocumentsResponse doc = ok(fineract().documents.getDocument("clients", clientId, documentId)); + assertThat(doc.getName()).isEqualTo(newName); + assertThat(doc.getDescription()).isEqualTo(newDescription); + } + + @Test + @Order(99) + void deleteDocument() throws IOException { + ok(fineract().documents.deleteDocument("clients", clientId, documentId)); + assertThat(fineract().documents.getDocument("clients", clientId, documentId)).hasHttpStatus(404); + } + + @Order(9999) + @Test // FINERACT-1036 + void createDocumentBadArgs() throws IOException { + assertThat(fineract().documents.createDocument("clients", 123L, null, "test.pdf", null)).hasHttpStatus(400); + } +} diff --git a/fineract-client/src/test/java/org/apache/fineract/integrationtests/newstyle/IntegrationTest.java b/fineract-client/src/test/java/org/apache/fineract/integrationtests/newstyle/IntegrationTest.java index 81559eed8..80e15eb1f 100644 --- a/fineract-client/src/test/java/org/apache/fineract/integrationtests/newstyle/IntegrationTest.java +++ b/fineract-client/src/test/java/org/apache/fineract/integrationtests/newstyle/IntegrationTest.java @@ -39,6 +39,8 @@ import org.apache.fineract.client.testutil.CallSubject; import org.apache.fineract.client.util.Calls; import org.apache.fineract.client.util.FineractClient; import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.TestMethodOrder; import retrofit2.Call; @@ -47,6 +49,8 @@ import retrofit2.Call; * * @author Michael Vorburger.ch */ +// Allow keeping state between tests +@TestInstance(Lifecycle.PER_CLASS) // TODO Remove @TestMethodOrder when https://github.com/junit-team/junit5/issues/1919 is available @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public abstract class IntegrationTest { @@ -60,7 +64,7 @@ public abstract class IntegrationTest { // TODO change from Fineract.dev to https://localhost:8443/fineract-provider/api/v1/ after FINERACT-1209 String url = System.getProperty("fineract.it.url", "https://demo.fineract.dev/fineract-provider/api/v1/"); fineract = FineractClient.builder().baseURL(url).tenant("default").basicAuth("mifos", "password").insecure(true) - .logging(Level.BODY).build(); + .logging(Level.NONE).build(); } return fineract; } diff --git a/fineract-client/src/test/resources/michael.vorburger-crepes.jpg b/fineract-client/src/test/resources/michael.vorburger-crepes.jpg new file mode 100644 index 000000000..c5f4bb6c5 Binary files /dev/null and b/fineract-client/src/test/resources/michael.vorburger-crepes.jpg differ diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/ApiParameterError.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/ApiParameterError.java index 67daff4e1..887ebcf62 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/ApiParameterError.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/ApiParameterError.java @@ -130,4 +130,9 @@ public final class ApiParameterError { public List getArgs() { return this.args; } + + @Override + public String toString() { + return "ApiParameterError{developerMessage=" + developerMessage + "; ... }"; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResource.java index 69811d082..8be3618ff 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResource.java @@ -103,7 +103,7 @@ public class DocumentManagementApiResource { + "client_identifiers/1/documents\n" + "\n" + "loans/1/documents?fields=name,description") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.GetEntityTypeEntityIdDocumentsResponse.class)))) }) - public String retreiveAllDocuments(@Context final UriInfo uriInfo, + public String retrieveAllDocuments(@Context final UriInfo uriInfo, @PathParam("entityType") @Parameter(description = "entityType") final String entityType, @PathParam("entityId") @Parameter(description = "entityId") final Long entityId) {