Skip to content

Commit

Permalink
Download Single Authority file by id and format
Browse files Browse the repository at this point in the history
  • Loading branch information
ShmElena committed Oct 1, 2024
1 parent 8d767e4 commit 147c881
Show file tree
Hide file tree
Showing 17 changed files with 446 additions and 6 deletions.
20 changes: 19 additions & 1 deletion descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,18 @@
"inventory-storage.authorities.collection.get",
"user-tenants.collection.get"
]
},
{
"methods": [
"GET"
],
"pathPattern": "/data-export/download-authority/{authorityId}",
"permissionsRequired": [
"data-export.download-authority.item.get"
],
"modulePermissions": [

]
}
]
},
Expand Down Expand Up @@ -649,6 +661,11 @@
"displayName": "Data Export - deleted authorities",
"description": "To return deleted authorities"
},
{
"permissionName": "data-export.download-authority.item.get",
"displayName": "Data Export - download authority",
"description": "Entry point to download single authority file in the specified format"
},
{
"permissionName": "data-export.all",
"displayName": "Data Export - all permissions",
Expand Down Expand Up @@ -680,7 +697,8 @@
"data-export.export.all",
"data-export.related-users.collection.get",
"data-export.export-deleted.post",
"data-export.export-authority-deleted.post"
"data-export.export-authority-deleted.post",
"data-export.download-authority.item.get"
],
"visible": false
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.folio.dataexp.controllers;


import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.folio.dataexp.rest.resource.DownloadAuthorityApi;
import org.folio.dataexp.service.DownloadAuthorityService;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@AllArgsConstructor
@Log4j2
@RequestMapping("/data-export")
public class DownloadAuthorityController implements DownloadAuthorityApi {

private static final String UTF_FORMAT_POSTFIX = "-utf";
private static final String MARC8_FORMAT_POSTFIX = "-marc8";

private final DownloadAuthorityService downloadAuthorityService;

@Override
public ResponseEntity<Resource> downloadAuthorityById(UUID authorityId, Boolean isUtf) {
var formatPostfix = Boolean.TRUE.equals(isUtf) ? UTF_FORMAT_POSTFIX : MARC8_FORMAT_POSTFIX;
ByteArrayResource resource = downloadAuthorityService.processAuthorityDownload(authorityId, isUtf, formatPostfix);
String fileName = authorityId + formatPostfix + ".mrc";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.folio.dataexp.exception.authority.AuthorityQueryException;
import org.folio.dataexp.exception.configuration.SliceSizeValidationException;
import org.folio.dataexp.exception.export.DataExportException;
import org.folio.dataexp.exception.export.DownloadRecordException;
import org.folio.dataexp.exception.export.ExportDeletedDateRangeException;
import org.folio.dataexp.exception.file.definition.FileExtensionException;
import org.folio.dataexp.exception.file.definition.FileSizeException;
Expand Down Expand Up @@ -103,4 +104,9 @@ public ResponseEntity<String> handleInvalidDateRangeException(final ExportDelete
public ResponseEntity<String> handleAuthorityQueryException(final AuthorityQueryException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(DownloadRecordException.class)
public ResponseEntity<String> handleDownloadRecordException(final DownloadRecordException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.folio.dataexp.exception.export;

public class DownloadRecordException extends RuntimeException {

public DownloadRecordException(String message) {
super(message);
}
}
117 changes: 117 additions & 0 deletions src/main/java/org/folio/dataexp/service/DownloadAuthorityService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.folio.dataexp.service;


import static org.folio.dataexp.service.export.Constants.OUTPUT_BUFFER_SIZE;
import static org.folio.dataexp.util.S3FilePathUtils.RECORD_LOCATION_PATH;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.folio.dataexp.domain.entity.MarcRecordEntity;
import org.folio.dataexp.exception.export.DownloadRecordException;
import org.folio.dataexp.repository.MappingProfileEntityRepository;
import org.folio.dataexp.service.export.LocalStorageWriter;
import org.folio.dataexp.service.export.S3ExportsUploader;
import org.folio.dataexp.service.export.strategies.AuthorityExportStrategy;
import org.folio.dataexp.service.export.strategies.JsonToMarcConverter;
import org.folio.dataexp.util.S3FilePathUtils;
import org.folio.spring.FolioExecutionContext;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.stereotype.Service;

@Service
@AllArgsConstructor
@Log4j2
public class DownloadAuthorityService {

private final AuthorityExportStrategy authorityExportStrategy;
private final MappingProfileEntityRepository mappingProfileEntityRepository;
private final JsonToMarcConverter jsonToMarcConverter;
private final S3ExportsUploader s3Uploader;
private final InputFileProcessor inputFileProcessor;
protected final FolioExecutionContext folioExecutionContext;

public ByteArrayResource processAuthorityDownload(final UUID authorityId, boolean isUtf, String formatPostfix) {
var dirName = authorityId.toString() + formatPostfix;
var marcFileContent = getContentIfFileExists(dirName);
if (marcFileContent.isEmpty()) {
marcFileContent = generateAuthorityFileContent(authorityId, isUtf);
saveMarcFile(dirName, marcFileContent);
}
return new ByteArrayResource(marcFileContent.getBytes());
}

private String getContentIfFileExists(final String dirName) {
try {
return inputFileProcessor.readMarcFile(dirName);
} catch (IOException e) {
log.error("getContentIfFileExists:: Error reading record from the storage in: {}", dirName);
throw new DownloadRecordException(e.getMessage());
}
}

private String generateAuthorityFileContent(final UUID authorityId, boolean isUtf) {
var marcAuthority = getMarcAuthority(authorityId);
var mappingProfile = mappingProfileEntityRepository.getReferenceById(
UUID.fromString("5d636597-a59d-4391-a270-4e79d5ba70e3")).getMappingProfile();
try {
return jsonToMarcConverter.convertJsonRecordToMarcRecord(marcAuthority.getContent(), List.of(),
mappingProfile, isUtf);
} catch (IOException e) {
log.error("createAuthorityFileContent :: Error generating content for authority with ID: {}", authorityId);
throw new DownloadRecordException(e.getMessage());
}
}

private MarcRecordEntity getMarcAuthority(final UUID authorityId) {
var marcAuthorities = authorityExportStrategy.getMarcAuthorities(Set.of(authorityId));
if (marcAuthorities.isEmpty()) {
log.error("getMarcAuthority:: Couldn't find authority in db for ID: {}", authorityId);
throw new DownloadRecordException("Couldn't find authority in db for ID: %s".formatted(authorityId));
}
marcAuthorities = authorityExportStrategy.handleDuplicatedDeletedAndUseLastGeneration(marcAuthorities);
return marcAuthorities.get(0);
}

private void saveMarcFile(final String dirName, final String marcFileContent) {
var localStorageWriterPath = saveMarcFileToLocalStorage(dirName, marcFileContent);
uploadMarcFile(dirName, localStorageWriterPath);
}

private String saveMarcFileToLocalStorage(final String dirName, final String marcFileContent) {
var localFileLocation = RECORD_LOCATION_PATH.formatted(dirName, dirName) + ".mrc";
var localStorageWriterPath = S3FilePathUtils.getLocalStorageWriterPath(
authorityExportStrategy.getExportTmpStorage(), localFileLocation);
createDirectoryForLocalStorage(dirName);
var localStorageWriter = new LocalStorageWriter(localStorageWriterPath, OUTPUT_BUFFER_SIZE);
localStorageWriter.write(marcFileContent);
localStorageWriter.close();
return localStorageWriterPath;
}

private void createDirectoryForLocalStorage(final String dirName) {
try {
Files.createDirectories(
Path.of(S3FilePathUtils.getTempDirForRecordId(authorityExportStrategy.getExportTmpStorage(), dirName)));
} catch (IOException e) {
log.error("createDirectoryForLocalStorage:: Can not create temp directory for record {}", dirName);
throw new DownloadRecordException(e.getMessage());
}
}

private void uploadMarcFile(String dirName, String localStorageWriterPath) {
var fileToUpload = new File(localStorageWriterPath);
try {
s3Uploader.uploadSingleRecordById(dirName, fileToUpload);
} catch (IOException e) {
log.error("uploadMarcFile:: Error while upload marc file {} to remote storage", localStorageWriterPath);
throw new DownloadRecordException(e.getMessage());
}
}
}
11 changes: 11 additions & 0 deletions src/main/java/org/folio/dataexp/service/InputFileProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.concurrent.atomic.AtomicInteger;

import static org.awaitility.Awaitility.await;
import static org.folio.dataexp.util.S3FilePathUtils.getPathToStoredRecord;

@Component
@RequiredArgsConstructor
Expand Down Expand Up @@ -68,6 +69,16 @@ public void readFile(FileDefinition fileDefinition, CommonExportStatistic common
}
}

public String readMarcFile(String dirName) throws IOException {
var pathToRead = getPathToStoredRecord(dirName, "%s.mrc".formatted(dirName));
if (s3Client.list(pathToRead).isEmpty()) {
return StringUtils.EMPTY;
}
try (InputStream is = s3Client.read(pathToRead)) {
return IOUtils.toString(is, StandardCharsets.UTF_8);
}
}

private void readCsvFile(FileDefinition fileDefinition, CommonExportStatistic commonExportStatistic) {
var jobExecution = jobExecutionService.getById(fileDefinition.getJobExecutionId());
var progress = jobExecution.getProgress();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private void updateJobExecutionStatusAndProgress(UUID jobExecutionId, ExportStra
var fileDefinition= queryResult.get(0).getFileDefinition();
var initialFileName= FilenameUtils.getBaseName(fileDefinition.getFileName());
try {
var innerFileName = s3Uploader.upload(jobExecution, filesForExport, initialFileName);
var innerFileName = s3Uploader.upload(jobExecution, filesForExport, initialFileName);//
var innerFile = new JobExecutionExportedFilesInner().fileId(UUID.randomUUID())
.fileName(FilenameUtils.getName(innerFileName));
jobExecution.setExportedFiles(Set.of(innerFile));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static org.folio.dataexp.util.S3FilePathUtils.getPathToStoredRecord;
import static org.folio.dataexp.util.S3FilePathUtils.getPathToStoredFiles;


Expand Down Expand Up @@ -68,6 +69,21 @@ public String upload(JobExecution jobExecution, List<JobExecutionExportFilesEnti
}
}

public void uploadSingleRecordById(String dirName, File fileToUpload) throws IOException {
var s3FileName = "%s.mrc".formatted(dirName);
var s3path = getPathToStoredRecord(dirName, s3FileName);
if (fileToUpload.length() > 0) {
try (var inputStream = new BufferedInputStream(new FileInputStream(fileToUpload))) {
s3Client.write(s3path, inputStream, fileToUpload.length());
}
log.info(fileToUpload.getPath() + " uploaded as " + s3FileName);
removeTempDirForRecord(dirName);
} else {
removeTempDirForRecord(dirName);
throw new S3ExportsUploadException(EMPTY_FILE_FOR_EXPORT_ERROR_MESSAGE);
}
}

private String uploadMarc(JobExecution jobExecution, File fileToUpload, String fileName) throws IOException {
var s3Name = String.format("%s-%s.mrc", fileName, jobExecution.getHrId());
var s3path = getPathToStoredFiles(jobExecution.getId(), s3Name);
Expand Down Expand Up @@ -120,4 +136,8 @@ private String uploadZip (JobExecution jobExecution, List<File> exports, String
private void removeTempDirForJobExecution(UUID jobExecutionId) throws IOException {
FileUtils.deleteDirectory(new File(S3FilePathUtils.getTempDirForJobExecutionId(exportTmpStorage, jobExecutionId)));
}

private void removeTempDirForRecord(final String dirName) throws IOException {
FileUtils.deleteDirectory(new File(S3FilePathUtils.getTempDirForRecordId(exportTmpStorage, dirName)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import net.minidev.json.JSONObject;
import net.minidev.json.parser.JSONParser;
Expand Down Expand Up @@ -46,6 +47,7 @@
import java.util.stream.Collectors;

@Log4j2
@Getter
public abstract class AbstractExportStrategy implements ExportStrategy {

protected int exportIdsBatch;
Expand Down Expand Up @@ -95,7 +97,7 @@ public ExportStrategyStatistic saveMarcToLocalStorage(JobExecutionExportFilesEnt
try {
localStorageWriter.close();
} catch (Exception e) {
log.error("saveMarcToRemoteStorage:: Error while saving file {} to local storage for job execution {}", exportFilesEntity.getFileLocation(), exportFilesEntity.getJobExecutionId());
log.error("saveMarcToLocalStorage:: Error while saving file {} to local storage for job execution {}", exportFilesEntity.getFileLocation(), exportFilesEntity.getJobExecutionId());
exportStatistic.setDuplicatedSrs(0);
exportStatistic.removeExported();
long countFailed = exportIdEntityRepository.countExportIds(exportFilesEntity.getJobExecutionId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public AuthorityExportAllStrategy(ConsortiaService consortiaService, ErrorLogEnt
}

@Override
protected List<MarcRecordEntity> getMarcAuthorities(Set<UUID> externalIds) {
public List<MarcRecordEntity> getMarcAuthorities(Set<UUID> externalIds) {
return marcAuthorityRecordRepository.findAllByExternalIdIn(context.getTenantId(), externalIds);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ List<MarcRecordEntity> getMarcRecords(Set<UUID> externalIds, MappingProfile mapp
return new ArrayList<>();
}

protected List<MarcRecordEntity> getMarcAuthorities(Set<UUID> externalIds) {
public List<MarcRecordEntity> getMarcAuthorities(Set<UUID> externalIds) {
return marcAuthorityRecordRepository.findNonDeletedByExternalIdIn(context.getTenantId(), externalIds);
}

Expand Down Expand Up @@ -122,7 +122,7 @@ private void handleDeleted(List<MarcRecordEntity> marcAuthorities, UUID jobExecu
}
}

private List<MarcRecordEntity> handleDuplicatedDeletedAndUseLastGeneration(List<MarcRecordEntity> marcAuthorities) {
public List<MarcRecordEntity> handleDuplicatedDeletedAndUseLastGeneration(List<MarcRecordEntity> marcAuthorities) {
return marcAuthorities.stream().collect(
groupingBy(MarcRecordEntity::getExternalId,
maxBy(comparing(MarcRecordEntity::getGeneration)))).values().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.marc4j.MarcException;
import org.marc4j.MarcJsonReader;
import org.marc4j.MarcStreamWriter;
import org.marc4j.converter.impl.UnicodeToAnsel;
import org.marc4j.marc.VariableField;
import org.marc4j.marc.Record;
import org.marc4j.marc.impl.SortedMarcFactoryImpl;
Expand All @@ -22,11 +23,19 @@
public class JsonToMarcConverter {

public String convertJsonRecordToMarcRecord(String jsonRecord, List<VariableField> additionalFields, MappingProfile mappingProfile) throws IOException {
return convertJsonRecordToMarcRecord(jsonRecord, additionalFields, mappingProfile, true);
}

public String convertJsonRecordToMarcRecord(String jsonRecord, List<VariableField> additionalFields, MappingProfile mappingProfile,
boolean isUtf) throws IOException {
var byteArrayInputStream = new ByteArrayInputStream(jsonRecord.getBytes(StandardCharsets.UTF_8));
var byteArrayOutputStream = new ByteArrayOutputStream();
try (byteArrayInputStream; byteArrayOutputStream) {
var marcJsonReader = new MarcJsonReader(byteArrayInputStream);
var marcStreamWriter = new MarcStreamWriter(byteArrayOutputStream, StandardCharsets.UTF_8.name());
if (!isUtf) {
marcStreamWriter.setConverter(new UnicodeToAnsel());
}
writeMarc(marcJsonReader, marcStreamWriter, additionalFields, mappingProfile);
return byteArrayOutputStream.toString();
} catch (IOException e) {
Expand Down
Loading

0 comments on commit 147c881

Please sign in to comment.