I have an Angular frontend app with a Java backend in which the user select a document and then saves it.
To do this, I've been sending a FormData object from the frontend to the backend with no issues.
Here's a snippet of what I currently have in the frontend that works to send the contents of a single file to the backend:
public postSaveDocument(entityType: DocumentEntityType, id: number, body: DocumentInputDto): Observable<Upload<UploadWithLockResultsDto>> {
const url = `${DocumentationService.baseUrl}/${getUrlSegment(entityType)}/${id}/documents`;
//Create a FormData object and set the values of the document
const formData = new FormData();
formData.append('documentId', this.documentId.toString());
formData.append('title', this.title);
formData.append('content', this.content, this.content.name);
return this.httpClient.post(url, formData,
{...DocumentationService.options, reportProgress: true, observe: 'events', responseType: 'json'})
.pipe(upload<UploadWithLockResultsDto>());
}
And here's what I do in the backend that works to save that file:
@PostMapping("/{documentEntityType}/{entityId}/documents")
public ResponseEntity<UploadWithLockResultsDto> saveDocument(
@PathVariable("documentEntityType") DocumentEntityType documentEntityType,
@PathVariable("entityId") Integer entityId,
DocumentInputDto documentInputDto,
) throws AuthException, IOException {
//Save the updates to the database
var uploadWithLockResultsDto = documentService.documentUpdate(documentInputDto, authentication.getName(), authentication.getPrincipal());
return handleUploadWithLockResults(uploadWithLockResultsDto);
}
And this works just fine to save a single document.
Now what I would like to do is allow the user to select more than one documents at a time, and save them in one fell swoop as they are sent to the back end.
So far no luck with this.
I've tried putting the data into an array of FormData objects in the front-end
public postSaveDocuments(entityType: DocumentEntityType, id: number, body: DocumentInputDto[]): Observable<Upload<UploadWithLockResultsDto>> {
const formDataArray: any[] = [];
body.forEach(documentInputDto => {
const formData = new FormData();
formData.append('documentId', this.documentId.toString());
formData.append('title', this.title);
formData.append('content', this.content, this.content.name);
formDataArray.push(formData);
});
return this.httpClient.post(url, formDataArray,
{...DocumentationService.options, reportProgress: true, observe: 'events', responseType: 'json'})
.pipe(upload<UploadWithLockResultsDto>());
}
And then I try to scoop it up in the backend as an array
@PostMapping("/{documentEntityType}/{entityId}/documents")
public ResponseEntity<UploadWithLockResultsDto> saveDocument(
@PathVariable("documentEntityType") DocumentEntityType documentEntityType,
@PathVariable("entityId") Integer entityId,
DocumentInputDto[] documentInputDtos,
) throws AuthException, IOException {
//Save the docs to the database
var uploadWithLockResultsDto = documentService.updateDocuments(documentInputDtos, authentication.getName(), authentication.getPrincipal());
return handleUploadWithLockResults(uploadWithLockResultsDto);
}
Not surprisingly this doesn't work. It never even gets to the backend. And I realize that an array of FormData objects probably isn't meant to be sent from the frontend to the backend.
Can anyone point me in the right direction though as how to modify the call such that more than one FormData objects are sent from the front to the back?
Thanks much.
You're on the right track, but sending multiple FormData objects in an array directly from the frontend to the backend is not going to work because the FormData object is meant to be sent as a single entity. Instead, you should send the documents as individual parts in a single FormData object using a multi-part form request.
Step 1: Frontend - Modify the FormData object to handle multiple files To send multiple files, you can append each document's data to the same FormData object with unique keys for each document. This will allow you to send multiple documents in one HTTP request.
Updated frontend code:
public postSaveDocuments(entityType: DocumentEntityType, id: number, body: DocumentInputDto[]): Observable<Upload<UploadWithLockResultsDto>> {
const url = `${DocumentationService.baseUrl}/${getUrlSegment(entityType)}/${id}/documents`;
// Create a single FormData object for all documents
const formData = new FormData();
// Loop through the document DTOs and append each one
body.forEach((documentInputDto, index) => {
formData.append(`documents[${index}].documentId`, this.documentId.toString());
formData.append(`documents[${index}].title`, this.title);
formData.append(`documents[${index}].content`, this.content, this.content.name);
});
return this.httpClient.post(url, formData, {
...DocumentationService.options,
reportProgress: true,
observe: 'events',
responseType: 'json'
}).pipe(upload<UploadWithLockResultsDto>());
}
Step 2: Backend - Handle the multi-part form data In the backend, you need to handle the multiple files and their associated metadata. You can do this by accepting the data as a MultipartFile array or a custom DTO, depending on how you prefer to structure the request. Since you are sending metadata and files, you can use a combination of @RequestParam and @ModelAttribute annotations to bind the form data and file content.
Updated backend code:
@PostMapping("/{documentEntityType}/{entityId}/documents")
public ResponseEntity<UploadWithLockResultsDto> saveDocuments(
@PathVariable("documentEntityType") DocumentEntityType documentEntityType,
@PathVariable("entityId") Integer entityId,
@RequestParam("documents") List<MultipartFile> documents,
@RequestParam Map<String, String> documentMetadata) throws AuthException, IOException {
// Parse metadata and files
List<DocumentInputDto> documentInputDtos = new ArrayList<>();
for (int i = 0; i < documents.size(); i++) {
MultipartFile file = documents.get(i);
String documentId = documentMetadata.get("documents[" + i + "].documentId");
String title = documentMetadata.get("documents[" + i + "].title");
DocumentInputDto documentInputDto = new DocumentInputDto();
documentInputDto.setDocumentId(Long.parseLong(documentId));
documentInputDto.setTitle(title);
documentInputDto.setContent(file); // Assuming the DocumentInputDto has a 'content' field for the file
documentInputDtos.add(documentInputDto);
}
// Save the documents to the database
var uploadWithLockResultsDto = documentService.updateDocuments(documentInputDtos, authentication.getName(), authentication.getPrincipal());
return handleUploadWithLockResults(uploadWithLockResultsDto);
}
Step 3: Update your DocumentInputDto
public class DocumentInputDto {
private Long documentId;
private String title;
private MultipartFile content; // The file being uploaded
// Getters and setters...`enter code here`
}