FINERACT-2169: Refactor document management api and update swagger doc;

This commit is contained in:
viktorpavlenko
2025-03-19 12:26:37 +01:00
committed by Adam Saghy
parent a0ffaa1c0b
commit ac8f62db51
11 changed files with 48 additions and 156 deletions
@@ -21,7 +21,7 @@ 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.DocumentData;
import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse;
import org.apache.fineract.client.models.PutEntityTypeEntityIdDocumentsResponse;
import retrofit2.Call;
@@ -102,8 +102,8 @@ public interface DocumentsApiFixed {
* @return Call<GetEntityTypeEntityIdDocumentsResponse>
*/
@GET("v1/{entityType}/{entityId}/documents/{documentId}")
Call<GetEntityTypeEntityIdDocumentsResponse> getDocument(@retrofit2.http.Path("entityType") String entityType,
@retrofit2.http.Path("entityId") Long entityId, @retrofit2.http.Path("documentId") Long documentId);
Call<DocumentData> 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
@@ -116,7 +116,7 @@ public interface DocumentsApiFixed {
* @return Call&lt;List&lt;GetEntityTypeEntityIdDocumentsResponse&gt;&gt;
*/
@GET("v1/{entityType}/{entityId}/documents")
Call<List<GetEntityTypeEntityIdDocumentsResponse>> retrieveAllDocuments(@retrofit2.http.Path("entityType") String entityType,
Call<List<DocumentData>> retrieveAllDocuments(@retrofit2.http.Path("entityType") String entityType,
@retrofit2.http.Path("entityId") Long entityId);
/**
@@ -18,12 +18,21 @@
*/
package org.apache.fineract.infrastructure.documentmanagement.data;
import java.io.Serial;
import java.io.Serializable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
/**
* Immutable data object representing a user document being managed on the platform.
*/
public class DocumentData {
@Getter
@RequiredArgsConstructor
public class DocumentData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final Long id;
private final String parentEntityType;
@@ -36,57 +45,7 @@ public class DocumentData {
private final String description;
private final Integer storageType;
public DocumentData(final Long id, final String parentEntityType, final Long parentEntityId, final String name, final String fileName,
final Long size, final String type, final String description, final String location, final Integer storageType) {
this.id = id;
this.parentEntityType = parentEntityType;
this.parentEntityId = parentEntityId;
this.name = name;
this.fileName = fileName;
this.size = size;
this.type = type;
this.description = description;
this.location = location;
this.storageType = storageType;
}
public String contentType() {
return this.type;
}
public String fileName() {
return this.fileName;
}
public String fileLocation() {
return this.location;
}
public StorageType storageType() {
return StorageType.fromInt(this.storageType);
}
public String getParentEntityType() {
return this.parentEntityType;
}
public Long getParentEntityId() {
return this.parentEntityId;
}
public Long getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getDescription() {
return this.description;
}
public Long getSize() {
return this.size;
}
}
@@ -20,7 +20,6 @@ package org.apache.fineract.infrastructure.documentmanagement.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
@@ -36,20 +35,14 @@ import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
@@ -70,16 +63,11 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor
public class DocumentManagementApiResource {
private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(
Arrays.asList("id", "parentEntityType", "parentEntityId", "name", "fileName", "size", "type", "description"));
private static final String SYSTEM_ENTITY_TYPE = "DOCUMENT";
private final PlatformSecurityContext context;
private final DocumentReadPlatformService documentReadPlatformService;
private final DocumentWritePlatformService documentWritePlatformService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final ToApiJsonSerializer<DocumentData> toApiJsonSerializer;
private final FileUploadValidator fileUploadValidator;
@GET
@@ -87,18 +75,11 @@ public class DocumentManagementApiResource {
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List documents", description = "Example Requests:\n" + "\n" + "clients/1/documents\n" + "\n"
+ "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 retrieveAllDocuments(@Context final UriInfo uriInfo,
@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
public List<DocumentData> retrieveAllDocuments(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId) {
this.context.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
final Collection<DocumentData> documentDatas = this.documentReadPlatformService.retrieveAllDocuments(entityType, entityId);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, documentDatas, RESPONSE_DATA_PARAMETERS);
context.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
return documentReadPlatformService.retrieveAllDocuments(entityType, entityId);
}
@POST
@@ -112,7 +93,7 @@ public class DocumentManagementApiResource {
+ "file and description")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)", content = @Content(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.PostEntityTypeEntityIdDocumentsResponse.class))) })
public String createDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
public CommandProcessingResult createDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
@HeaderParam("Content-Length") @Parameter(description = "Content-Length") final Long fileSize,
@FormDataParam("file") final InputStream inputStream, @FormDataParam("file") final FormDataContentDisposition fileDetails,
@@ -125,8 +106,8 @@ public class DocumentManagementApiResource {
fileUploadValidator.validate(fileSize, inputStream, fileDetails, bodyPart);
final DocumentCommand documentCommand = new DocumentCommand(null, null, entityType, entityId, name, fileDetails.getFileName(),
fileSize, bodyPart.getMediaType().toString(), description, null);
final Long documentId = this.documentWritePlatformService.createDocument(documentCommand, inputStream);
return this.toApiJsonSerializer.serialize(CommandProcessingResult.resourceResult(documentId));
final Long documentId = documentWritePlatformService.createDocument(documentCommand, inputStream);
return CommandProcessingResult.resourceResult(documentId);
}
@PUT
@@ -140,7 +121,7 @@ public class DocumentManagementApiResource {
+ "The file to be uploaded")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)", content = @Content(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.PutEntityTypeEntityIdDocumentsResponse.class))) })
public String updateDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
public CommandProcessingResult updateDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId,
@HeaderParam("Content-Length") @Parameter(description = "Content-Length") final Long fileSize,
@@ -155,7 +136,7 @@ public class DocumentManagementApiResource {
/***
* Populate Document command based on whether a file has also been passed in as a part of the update
***/
DocumentCommand documentCommand = null;
DocumentCommand documentCommand;
if (inputStream != null && fileDetails.getFileName() != null) {
fileUploadValidator.validate(fileSize, inputStream, fileDetails, bodyPart);
modifiedParams.add("fileName");
@@ -171,9 +152,7 @@ public class DocumentManagementApiResource {
/***
* TODO: does not return list of changes, should be done for consistency with rest of API
**/
final CommandProcessingResult identifier = this.documentWritePlatformService.updateDocument(documentCommand, inputStream);
return this.toApiJsonSerializer.serialize(identifier);
return documentWritePlatformService.updateDocument(documentCommand, inputStream);
}
@GET
@@ -182,18 +161,12 @@ public class DocumentManagementApiResource {
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a Document", description = "Example Requests:\n" + "\n" + "clients/1/documents/1\n" + "\n" + "\n"
+ "loans/1/documents/1\n" + "\n" + "\n" + "client_identifiers/1/documents/1?fields=name,description")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.GetEntityTypeEntityIdDocumentsResponse.class))) })
public String getDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
public DocumentData getDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId, @Context final UriInfo uriInfo) {
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId) {
this.context.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
final DocumentData documentData = this.documentReadPlatformService.retrieveDocument(entityType, entityId, documentId);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, documentData, RESPONSE_DATA_PARAMETERS);
context.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
return documentReadPlatformService.retrieveDocument(entityType, entityId, documentId);
}
@GET
@@ -207,8 +180,8 @@ public class DocumentManagementApiResource {
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId) {
this.context.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
final FileData fileData = this.documentReadPlatformService.retrieveFileData(entityType, entityId, documentId);
context.authenticatedUser().validateHasReadPermission(SYSTEM_ENTITY_TYPE);
final FileData fileData = documentReadPlatformService.retrieveFileData(entityType, entityId, documentId);
return ContentResources.fileDataToResponse(fileData, "attachment");
}
@@ -219,15 +192,13 @@ public class DocumentManagementApiResource {
@Operation(summary = "Remove a Document", description = "")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DocumentManagementApiResourceSwagger.DeleteEntityTypeEntityIdDocumentsResponse.class))) })
public String deleteDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
public CommandProcessingResult deleteDocument(@PathParam("entityType") @Parameter(description = "entityType") final String entityType,
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId) {
final DocumentCommand documentCommand = new DocumentCommand(null, documentId, entityType, entityId, null, null, null, null, null,
null);
final CommandProcessingResult documentIdentifier = this.documentWritePlatformService.deleteDocument(documentCommand);
return this.toApiJsonSerializer.serialize(documentIdentifier);
return documentWritePlatformService.deleteDocument(documentCommand);
}
}
@@ -32,35 +32,6 @@ final class DocumentManagementApiResourceSwagger {
}
@Schema(description = "GetEntityTypeEntityIdDocumentsResponse")
public static final class GetEntityTypeEntityIdDocumentsResponse {
private GetEntityTypeEntityIdDocumentsResponse() {
}
@Schema(example = "1")
public Long id;
@Schema(example = "clients")
public String parentEntityType;
@Schema(example = "1")
public Long parentEntityId;
@Schema(example = "Client Details Form")
public String name;
@Schema(example = "CGAP.pdf")
public String fileName;
@Schema(example = "5246719")
public Long size;
@Schema(example = "application/pdf")
public String type;
@Schema(example = "A signed form signed by new member")
public String description;
@Schema(example = "")
public String location;
@Schema(example = "")
public Integer storageType;
}
@Schema(description = "PostEntityTypeEntityIdDocumentsResponse")
public static final class PostEntityTypeEntityIdDocumentsResponse {
@@ -103,17 +74,8 @@ final class DocumentManagementApiResourceSwagger {
}
public static final class PutEntityTypeEntityIdDocumentsResponseChangesSwagger {
private PutEntityTypeEntityIdDocumentsResponseChangesSwagger() {
}
}
@Schema(example = "3")
public Long resourceId;
public DeleteEntityTypeEntityIdDocumentsResponse.PutEntityTypeEntityIdDocumentsResponseChangesSwagger changes;
@Schema(example = "3")
public String resourceIdentifier;
}
@@ -104,10 +104,10 @@ public class FileSystemContentRepository implements ContentRepository {
@Override
public FileData fetchFile(final DocumentData documentData) {
String sanitizedPath = pathSanitizer.sanitize(documentData.fileLocation());
String sanitizedPath = pathSanitizer.sanitize(documentData.getLocation());
final File file = new File(sanitizedPath);
return new FileData(Files.asByteSource(file), documentData.fileName(), documentData.contentType());
return new FileData(Files.asByteSource(file), documentData.getFileName(), documentData.getType());
}
@Override
@@ -108,9 +108,9 @@ public class S3ContentRepository implements ContentRepository {
@Override
public InputStream openStream() throws IOException {
return s3Client.getObject(GetObjectRequest.builder().bucket(fineractProperties.getContent().getS3().getBucketName())
.key(documentData.fileLocation()).build(), ResponseTransformer.toBytes()).asInputStream();
.key(documentData.getLocation()).build(), ResponseTransformer.toBytes()).asInputStream();
}
}, documentData.fileName(), documentData.contentType());
}, documentData.getFileName(), documentData.getType());
}
@Override
@@ -18,13 +18,13 @@
*/
package org.apache.fineract.infrastructure.documentmanagement.service;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.apache.fineract.infrastructure.documentmanagement.data.FileData;
public interface DocumentReadPlatformService {
Collection<DocumentData> retrieveAllDocuments(String entityType, Long entityId);
List<DocumentData> retrieveAllDocuments(String entityType, Long entityId);
FileData retrieveFileData(String entityType, Long entityId, Long documentId);
@@ -20,7 +20,7 @@ package org.apache.fineract.infrastructure.documentmanagement.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.ContentRepository;
@@ -43,7 +43,7 @@ public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformServ
private final ContentRepositoryFactory contentRepositoryFactory;
@Override
public Collection<DocumentData> retrieveAllDocuments(final String entityType, final Long entityId) {
public List<DocumentData> retrieveAllDocuments(final String entityType, final Long entityId) {
this.context.authenticatedUser();
@@ -118,7 +118,7 @@ public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformServ
if (!this.hideStorageType) {
storageType = rs.getInt("storageType");
}
return new DocumentData(id, parentEntityType, parentEntityId, name, fileName, fileSize, fileType, description, location,
return new DocumentData(id, parentEntityType, parentEntityId, name, fileName, fileSize, fileType, location, description,
storageType);
}
}
@@ -87,7 +87,7 @@ public class BulkImportApiResource {
public String retriveOutputTemplateLocation(@QueryParam("importDocumentId") final String importDocumentId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSION);
final DocumentData documentData = this.bulkImportWorkbookService.getOutputTemplateLocation(importDocumentId);
return this.toApiJsonSerializer.serialize(documentData.fileLocation());
return this.toApiJsonSerializer.serialize(documentData.getLocation());
}
@GET
@@ -260,8 +260,8 @@ public class BulkImportWorkbookServiceImpl implements BulkImportWorkbookService
}
private Response buildResponse(DocumentData documentData) {
String fileName = "Output" + documentData.fileName();
String fileLocation = documentData.fileLocation();
String fileName = "Output" + documentData.getFileName();
String fileLocation = documentData.getLocation();
File file = new File(fileLocation);
final Response.ResponseBuilder response = Response.ok(file);
response.header("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
@@ -282,7 +282,7 @@ public class BulkImportWorkbookServiceImpl implements BulkImportWorkbookService
public DocumentData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
final String location = rs.getString("location");
final String fileName = rs.getString("file_name");
return new DocumentData(null, null, null, null, fileName, null, null, null, location, null);
return new DocumentData(null, null, null, null, fileName, null, null, location, null, null);
}
}
}
@@ -26,7 +26,7 @@ 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.DocumentData;
import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.client.util.Parts;
import org.junit.jupiter.api.Order;
@@ -66,7 +66,7 @@ public class DocumentTest extends IntegrationTest {
@Test
@Order(3)
void getDocument() {
GetEntityTypeEntityIdDocumentsResponse doc = ok(fineractClient().documents.getDocument("clients", clientId, documentId));
DocumentData doc = ok(fineractClient().documents.getDocument("clients", clientId, documentId));
assertThat(doc.getName()).isEqualTo("Test");
assertThat(doc.getFileName()).isEqualTo(testFile.getName());
assertThat(doc.getDescription()).isEqualTo("The Description");
@@ -99,7 +99,7 @@ public class DocumentTest extends IntegrationTest {
String newDescription = getClass().getName();
ok(fineractClient().documents.updateDocument("clients", clientId, documentId, null, newName, newDescription));
GetEntityTypeEntityIdDocumentsResponse doc = ok(fineractClient().documents.getDocument("clients", clientId, documentId));
DocumentData doc = ok(fineractClient().documents.getDocument("clients", clientId, documentId));
assertThat(doc.getName()).isEqualTo(newName);
assertThat(doc.getDescription()).isEqualTo(newDescription);
// TODO FINERACT-1251 It's more than uploaded file; seems like a bug - it's including create body, not just file