From 197a6765a0d311abb8fc47519746e8651365f449 Mon Sep 17 00:00:00 2001 From: Dmytro_Bykov Date: Fri, 29 Dec 2023 18:13:41 +0500 Subject: [PATCH 1/3] MODEXPW-452: Retrieve instance records for bulk edit. Initial commit --- descriptors/ModuleDescriptor-template.json | 7 +- .../jobs/BulkEditInstanceListProcessor.java | 34 +++++ .../jobs/BulkEditInstanceProcessor.java | 120 +++++++++++++++++ .../BulkEditInstanceIdentifiersJobConfig.java | 91 +++++++++++++ .../processidentifiers/InstanceFetcher.java | 52 ++++++++ .../dew/client/InstanceFormatsClient.java | 14 ++ .../client/InstanceModeOfIssuanceClient.java | 14 ++ .../dew/client/InstanceStatusesClient.java | 14 ++ .../folio/dew/client/InstanceTypesClient.java | 13 ++ .../dew/client/InventoryInstancesClient.java | 30 +++++ .../client/NatureOfContentTermsClient.java | 14 ++ .../dew/controller/BulkEditController.java | 4 + .../folio/dew/domain/dto/InstanceFormat.java | 124 ++++++++++++++++++ .../dew/service/InstanceReferenceService.java | 75 +++++++++++ src/main/resources/swagger.api/bulk-edit.yaml | 18 +++ 15 files changed, 623 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/folio/dew/batch/bulkedit/jobs/BulkEditInstanceListProcessor.java create mode 100644 src/main/java/org/folio/dew/batch/bulkedit/jobs/BulkEditInstanceProcessor.java create mode 100644 src/main/java/org/folio/dew/batch/bulkedit/jobs/processidentifiers/BulkEditInstanceIdentifiersJobConfig.java create mode 100644 src/main/java/org/folio/dew/batch/bulkedit/jobs/processidentifiers/InstanceFetcher.java create mode 100644 src/main/java/org/folio/dew/client/InstanceFormatsClient.java create mode 100644 src/main/java/org/folio/dew/client/InstanceModeOfIssuanceClient.java create mode 100644 src/main/java/org/folio/dew/client/InstanceStatusesClient.java create mode 100644 src/main/java/org/folio/dew/client/InstanceTypesClient.java create mode 100644 src/main/java/org/folio/dew/client/InventoryInstancesClient.java create mode 100644 src/main/java/org/folio/dew/client/NatureOfContentTermsClient.java create mode 100644 src/main/java/org/folio/dew/domain/dto/InstanceFormat.java create mode 100644 src/main/java/org/folio/dew/service/InstanceReferenceService.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 3fb4d7e2e..ed5d4da18 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -184,7 +184,12 @@ "usergroups.collection.get", "usergroups.item.get", "users.collection.get", - "users.item.get" + "users.item.get", + "inventory-storage.instance-statuses.item.get", + "inventory-storage.modes-of-issuance.item.get", + "inventory-storage.instance-types.item.get", + "inventory-storage.nature-of-content-terms.item.get", + "inventory-storage.instance-formats.item.get" ] }, { diff --git a/src/main/java/org/folio/dew/batch/bulkedit/jobs/BulkEditInstanceListProcessor.java b/src/main/java/org/folio/dew/batch/bulkedit/jobs/BulkEditInstanceListProcessor.java new file mode 100644 index 000000000..844c799d1 --- /dev/null +++ b/src/main/java/org/folio/dew/batch/bulkedit/jobs/BulkEditInstanceListProcessor.java @@ -0,0 +1,34 @@ +package org.folio.dew.batch.bulkedit.jobs; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.dew.domain.dto.InstanceCollection; +import org.folio.dew.domain.dto.InstanceFormat; +import org.folio.dew.error.BulkEditException; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.folio.dew.utils.Constants.NO_MATCH_FOUND_MESSAGE; + +@Component +@StepScope +@RequiredArgsConstructor +@Log4j2 +public class BulkEditInstanceListProcessor implements ItemProcessor> { + private final BulkEditInstanceProcessor bulkEditInstanceProcessor; + + @Override + public List process(InstanceCollection instances) { + if (instances.getInstances().isEmpty()) { + log.error(NO_MATCH_FOUND_MESSAGE); + throw new BulkEditException(NO_MATCH_FOUND_MESSAGE); + } + return instances.getInstances().stream() + .map(bulkEditInstanceProcessor::process) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/folio/dew/batch/bulkedit/jobs/BulkEditInstanceProcessor.java b/src/main/java/org/folio/dew/batch/bulkedit/jobs/BulkEditInstanceProcessor.java new file mode 100644 index 000000000..9dca6a3fb --- /dev/null +++ b/src/main/java/org/folio/dew/batch/bulkedit/jobs/BulkEditInstanceProcessor.java @@ -0,0 +1,120 @@ +package org.folio.dew.batch.bulkedit.jobs; + +import static org.apache.commons.lang3.ObjectUtils.isEmpty; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.io.FilenameUtils; +import org.folio.dew.domain.dto.*; +import org.folio.dew.domain.dto.FormatOfInstance; +import org.folio.dew.domain.dto.Instance; +import org.folio.dew.domain.dto.InstanceContributorsInner; +import org.folio.dew.domain.dto.InstanceSeriesInner; +import org.folio.dew.service.InstanceReferenceService; +import org.folio.dew.service.SpecialCharacterEscaper; +import org.jetbrains.annotations.NotNull; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.folio.dew.utils.Constants.ITEM_DELIMITER; + +@Component +@StepScope +@RequiredArgsConstructor +@Log4j2 +public class BulkEditInstanceProcessor implements ItemProcessor { + + private final InstanceReferenceService instanceReferenceService; + private final SpecialCharacterEscaper escaper; + + + @Value("#{jobParameters['identifierType']}") + private String identifierType; + @Value("#{jobParameters['jobId']}") + private String jobId; + @Value("#{jobParameters['fileName']}") + private String fileName; + + @Override + public InstanceFormat process(@NotNull Instance instance) { + var errorServiceArgs = new ErrorServiceArgs(jobId, getIdentifier(instance, identifierType), FilenameUtils.getName(fileName)); + + var instanceFormat = InstanceFormat.builder() + .id(instance.getId()) + .discoverySuppress(isEmpty(instance.getVersion()) ? EMPTY : Boolean.toString(instance.getDiscoverySuppress())) + .staffSuppress(isEmpty(instance.getStaffSuppress()) ? EMPTY : Boolean.toString(instance.getStaffSuppress())) + .previouslyHeld(isEmpty(instance.getPreviouslyHeld()) ? EMPTY : Boolean.toString(instance.getPreviouslyHeld())) + .hrid(instance.getHrid()) + .source(instance.getSource()) + .catalogedDate(instance.getCatalogedDate()) + .statusId(instanceReferenceService.getInstanceStatusNameById(instance.getStatusId(), errorServiceArgs)) + .modeOfIssuanceId(instanceReferenceService.getModeOfIssuanceNameById(instance.getModeOfIssuanceId(), errorServiceArgs)) + .administrativeNotes(isEmpty(instance.getAdministrativeNotes()) ? EMPTY : String.join(ITEM_DELIMITER, escaper.escape(instance.getAdministrativeNotes()))) + .title(instance.getTitle()) + .indexTitle(instance.getIndexTitle()) + .series(fetchSeries(instance.getSeries())) + .contributors(fetchContributorNames(instance.getContributors())) + .editions(isEmpty(instance.getEditions()) ? EMPTY : String.join(ITEM_DELIMITER, escaper.escape(new ArrayList<>(instance.getEditions())))) + .physicalDescriptions(isEmpty(instance.getPhysicalDescriptions()) ? EMPTY : String.join(ITEM_DELIMITER, escaper.escape(instance.getPhysicalDescriptions()))) + .instanceTypeId(instanceReferenceService.getInstanceTypeNameById(instance.getInstanceTypeId(), errorServiceArgs)) + .natureOfContentTermIds(fetchNatureOfContentTerms(instance.getNatureOfContentTermIds(), errorServiceArgs)) + .instanceFormatIds(fetchInstanceFormats(instance.getInstanceFormats(), errorServiceArgs)) + .languages(isEmpty(instance.getLanguages()) ? EMPTY : String.join(ITEM_DELIMITER, escaper.escape(instance.getLanguages()))) + .publicationFrequency(isEmpty(instance.getPublicationFrequency()) ? EMPTY : String.join(ITEM_DELIMITER, escaper.escape(new ArrayList<>(instance.getPublicationFrequency())))) + .publicationRange(isEmpty(instance.getPublicationRange()) ? EMPTY : String.join(ITEM_DELIMITER, escaper.escape(new ArrayList<>(instance.getPublicationRange())))) + .build(); + + + return instanceFormat.withOriginal(instance); + } + + private String fetchInstanceFormats(List instanceFormats, ErrorServiceArgs errorServiceArgs) { + return isEmpty(instanceFormats) ? EMPTY : + instanceFormats.stream() + .map(iFormat -> instanceReferenceService.getFormatOfInstanceNameById(iFormat.getId(), errorServiceArgs)) + .map(iFormatName -> String.join(ITEM_DELIMITER, escaper.escape(iFormatName))) + .collect(Collectors.joining(ITEM_DELIMITER)); + } + + private String fetchNatureOfContentTerms(Set natureOfContentTermIds, ErrorServiceArgs errorServiceArgs) { + return isEmpty(natureOfContentTermIds) ? EMPTY : + natureOfContentTermIds.stream() + .map(natId -> instanceReferenceService.getNatureOfContentTermNameById(natId, errorServiceArgs)) + .map(natName -> String.join(ITEM_DELIMITER, escaper.escape(natName))) + .collect(Collectors.joining(ITEM_DELIMITER)); + } + + private String fetchContributorNames(List contributors) { + return isEmpty(contributors) ? EMPTY : + contributors.stream() + .map(c -> String.join(ITEM_DELIMITER, escaper.escape(c.getName()))) + .collect(Collectors.joining(ITEM_DELIMITER)); + } + + private String fetchSeries(Set series) { + return isEmpty(series) ? EMPTY : + series.stream() + .map(instanceSeriesInner -> String.join(ITEM_DELIMITER, escaper.escape(instanceSeriesInner.getValue()))) + .collect(Collectors.joining(ITEM_DELIMITER)); + } + + + private String getIdentifier(Instance instance, String identifierType) { + try { + return switch (org.folio.dew.domain.dto.IdentifierType.fromValue(identifierType)) { + case HRID -> instance.getHrid(); + default -> instance.getId(); + }; + } catch (IllegalArgumentException e) { + return instance.getId(); + } + } +} diff --git a/src/main/java/org/folio/dew/batch/bulkedit/jobs/processidentifiers/BulkEditInstanceIdentifiersJobConfig.java b/src/main/java/org/folio/dew/batch/bulkedit/jobs/processidentifiers/BulkEditInstanceIdentifiersJobConfig.java new file mode 100644 index 000000000..653b71e4e --- /dev/null +++ b/src/main/java/org/folio/dew/batch/bulkedit/jobs/processidentifiers/BulkEditInstanceIdentifiersJobConfig.java @@ -0,0 +1,91 @@ +package org.folio.dew.batch.bulkedit.jobs.processidentifiers; + + +import lombok.RequiredArgsConstructor; +import org.folio.dew.batch.CsvListFileWriter; +import org.folio.dew.batch.JsonListFileWriter; +import org.folio.dew.batch.JobCompletionNotificationListener; +import org.folio.dew.batch.bulkedit.jobs.BulkEditInstanceListProcessor; +import org.folio.dew.domain.dto.ExportType; +import org.folio.dew.domain.dto.ItemIdentifier; +import org.folio.dew.domain.dto.InstanceFormat; +import org.folio.dew.error.BulkEditException; +import org.folio.dew.error.BulkEditSkipListener; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.file.FlatFileItemReader; +import org.springframework.batch.item.support.CompositeItemProcessor; +import org.springframework.batch.item.support.CompositeItemWriter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.FileSystemResource; +import org.springframework.transaction.PlatformTransactionManager; + +import java.util.Arrays; +import java.util.List; + +import static org.folio.dew.domain.dto.EntityType.INSTANCE; +import static org.folio.dew.domain.dto.JobParameterNames.TEMP_LOCAL_FILE_PATH; +import static org.folio.dew.utils.Constants.CHUNKS; +import static org.folio.dew.utils.Constants.JOB_NAME_POSTFIX_SEPARATOR; + +@Configuration +@RequiredArgsConstructor +public class BulkEditInstanceIdentifiersJobConfig { + + private final BulkEditInstanceListProcessor bulkEditInstanceListProcessor; + private final InstanceFetcher instanceFetcher; + private final BulkEditSkipListener bulkEditSkipListener; + + @Bean + public Job bulkEditProcessInstanceIdentifiersJob(JobCompletionNotificationListener listener, Step bulkEditInstanceStep, + JobRepository jobRepository) { + return new JobBuilder(ExportType.BULK_EDIT_IDENTIFIERS + JOB_NAME_POSTFIX_SEPARATOR + INSTANCE.getValue(), jobRepository) + .incrementer(new RunIdIncrementer()) + .listener(listener) + .flow(bulkEditInstanceStep) + .end() + .build(); + } + + @Bean + public Step bulkEditInstanceStep(FlatFileItemReader csvItemIdentifierReader, + CompositeItemWriter> compositeInstanceListWriter, + ListIdentifiersWriteListener listIdentifiersWriteListener, JobRepository jobRepository, + PlatformTransactionManager transactionManager) { + return new StepBuilder("bulkEditInstanceStep", jobRepository) + .> chunk(CHUNKS, transactionManager) + .reader(csvItemIdentifierReader) + .processor(identifierInstanceProcessor()) + .faultTolerant() + .skipLimit(1_000_000) + .processorNonTransactional() // Required to avoid repeating BulkEditItemProcessor#process after skip. + .skip(BulkEditException.class) + .listener(bulkEditSkipListener) + .writer(compositeInstanceListWriter) + .listener(listIdentifiersWriteListener) + .build(); + } + + @Bean + public CompositeItemProcessor> identifierInstanceProcessor() { + var processor = new CompositeItemProcessor>(); + processor.setDelegates(Arrays.asList(instanceFetcher, bulkEditInstanceListProcessor)); + return processor; + } + + @Bean + @StepScope + public CompositeItemWriter> compositeInstanceListWriter(@Value("#{jobParameters['" + TEMP_LOCAL_FILE_PATH + "']}") String outputFileName) { + var writer = new CompositeItemWriter>(); + writer.setDelegates(Arrays.asList(new CsvListFileWriter<>(outputFileName, InstanceFormat.getInstanceColumnHeaders(), InstanceFormat.getInstanceFieldsArray(), (field, i) -> field), + new JsonListFileWriter<>(new FileSystemResource(outputFileName + ".json")))); + return writer; + } +} diff --git a/src/main/java/org/folio/dew/batch/bulkedit/jobs/processidentifiers/InstanceFetcher.java b/src/main/java/org/folio/dew/batch/bulkedit/jobs/processidentifiers/InstanceFetcher.java new file mode 100644 index 000000000..651879f41 --- /dev/null +++ b/src/main/java/org/folio/dew/batch/bulkedit/jobs/processidentifiers/InstanceFetcher.java @@ -0,0 +1,52 @@ +package org.folio.dew.batch.bulkedit.jobs.processidentifiers; + +import feign.codec.DecodeException; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.dew.client.InventoryInstancesClient; +import org.folio.dew.domain.dto.IdentifierType; +import org.folio.dew.domain.dto.InstanceCollection; +import org.folio.dew.domain.dto.ItemIdentifier; +import org.folio.dew.error.BulkEditException; +import org.folio.dew.utils.ExceptionHelper; +import org.jetbrains.annotations.NotNull; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.Set; + +import static org.folio.dew.domain.dto.IdentifierType.HOLDINGS_RECORD_ID; +import static org.folio.dew.utils.BulkEditProcessorHelper.getMatchPattern; +import static org.folio.dew.utils.BulkEditProcessorHelper.resolveIdentifier; + +@Component +@StepScope +@RequiredArgsConstructor +@Log4j2 +public class InstanceFetcher implements ItemProcessor { + private final InventoryInstancesClient inventoryInstancesClient; + + @Value("#{jobParameters['identifierType']}") + private String identifierType; + + private final Set identifiersToCheckDuplication = new HashSet<>(); + + @Override + public InstanceCollection process(@NotNull ItemIdentifier itemIdentifier) throws BulkEditException { + if (identifiersToCheckDuplication.contains(itemIdentifier)) { + throw new BulkEditException("Duplicate entry"); + } + identifiersToCheckDuplication.add(itemIdentifier); + var limit = HOLDINGS_RECORD_ID == IdentifierType.fromValue(identifierType) ? Integer.MAX_VALUE : 1; + var idType = resolveIdentifier(identifierType); + var identifier = "barcode".equals(idType) ? Utils.encode(itemIdentifier.getItemId()) : itemIdentifier.getItemId(); + try { + return inventoryInstancesClient.getInstanceByQuery(String.format(getMatchPattern(identifierType), idType, identifier), limit); + } catch (DecodeException e) { + throw new BulkEditException(ExceptionHelper.fetchMessage(e)); + } + } +} diff --git a/src/main/java/org/folio/dew/client/InstanceFormatsClient.java b/src/main/java/org/folio/dew/client/InstanceFormatsClient.java new file mode 100644 index 000000000..b34be0fb7 --- /dev/null +++ b/src/main/java/org/folio/dew/client/InstanceFormatsClient.java @@ -0,0 +1,14 @@ +package org.folio.dew.client; + +import org.folio.dew.config.feign.FeignClientConfiguration; +import org.folio.dew.domain.dto.FormatOfInstance; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient(name = "instance-formats", configuration = FeignClientConfiguration.class) +public interface InstanceFormatsClient { + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + FormatOfInstance getById(@PathVariable String id); +} diff --git a/src/main/java/org/folio/dew/client/InstanceModeOfIssuanceClient.java b/src/main/java/org/folio/dew/client/InstanceModeOfIssuanceClient.java new file mode 100644 index 000000000..483fed14a --- /dev/null +++ b/src/main/java/org/folio/dew/client/InstanceModeOfIssuanceClient.java @@ -0,0 +1,14 @@ +package org.folio.dew.client; + +import org.folio.dew.config.feign.FeignClientConfiguration; +import org.folio.dew.domain.dto.IssuanceMode; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient(name = "modes-of-issuance", configuration = FeignClientConfiguration.class) +public interface InstanceModeOfIssuanceClient { + @GetMapping(value = "/{modeOfIssuanceId}", produces = MediaType.APPLICATION_JSON_VALUE) + IssuanceMode getById(@PathVariable String modeOfIssuanceId); +} diff --git a/src/main/java/org/folio/dew/client/InstanceStatusesClient.java b/src/main/java/org/folio/dew/client/InstanceStatusesClient.java new file mode 100644 index 000000000..877fa19ba --- /dev/null +++ b/src/main/java/org/folio/dew/client/InstanceStatusesClient.java @@ -0,0 +1,14 @@ +package org.folio.dew.client; + +import org.folio.dew.config.feign.FeignClientConfiguration; +import org.folio.dew.domain.dto.InstanceStatus; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient(name = "instance-statuses", configuration = FeignClientConfiguration.class) +public interface InstanceStatusesClient { + @GetMapping(value = "/{instanceStatusId}", produces = MediaType.APPLICATION_JSON_VALUE) + InstanceStatus getById(@PathVariable String instanceStatusId); +} diff --git a/src/main/java/org/folio/dew/client/InstanceTypesClient.java b/src/main/java/org/folio/dew/client/InstanceTypesClient.java new file mode 100644 index 000000000..ee81942ee --- /dev/null +++ b/src/main/java/org/folio/dew/client/InstanceTypesClient.java @@ -0,0 +1,13 @@ +package org.folio.dew.client; + +import org.folio.dew.config.feign.FeignClientConfiguration; +import org.folio.dew.domain.dto.InstanceType; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +@FeignClient(name = "instance-types", configuration = FeignClientConfiguration.class) +public interface InstanceTypesClient { + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + InstanceType getById(@PathVariable String id); +} diff --git a/src/main/java/org/folio/dew/client/InventoryInstancesClient.java b/src/main/java/org/folio/dew/client/InventoryInstancesClient.java new file mode 100644 index 000000000..4a097da82 --- /dev/null +++ b/src/main/java/org/folio/dew/client/InventoryInstancesClient.java @@ -0,0 +1,30 @@ +package org.folio.dew.client; + +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import org.folio.dew.config.feign.FeignEncoderConfiguration; +import org.folio.dew.domain.dto.Instance; +import org.folio.dew.domain.dto.InstanceCollection; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "inventory/instances", configuration = FeignEncoderConfiguration.class) +public interface InventoryInstancesClient { + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + InstanceCollection getInstanceByQuery(@RequestParam String query); + + @GetMapping(value = "/{instanceId}", produces = MediaType.APPLICATION_JSON_VALUE) + Instance getInstanceById(@PathVariable String instanceId); + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + InstanceCollection getInstanceByQuery(@RequestParam("query") String query, @RequestParam long limit); + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + InstanceCollection getInstanceByQuery(@RequestParam("query") String query, @RequestParam long offset, @RequestParam long limit); + + @PutMapping(value = "/{instanceId}", consumes = MediaType.APPLICATION_JSON_VALUE) + void updateInstance(@RequestBody Instance instance, @PathVariable String instanceId); +} diff --git a/src/main/java/org/folio/dew/client/NatureOfContentTermsClient.java b/src/main/java/org/folio/dew/client/NatureOfContentTermsClient.java new file mode 100644 index 000000000..28e6d972d --- /dev/null +++ b/src/main/java/org/folio/dew/client/NatureOfContentTermsClient.java @@ -0,0 +1,14 @@ +package org.folio.dew.client; + +import org.folio.dew.config.feign.FeignClientConfiguration; +import org.folio.dew.domain.dto.NatureOfContentTerm; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient(name = "nature-of-content-terms", configuration = FeignClientConfiguration.class) +public interface NatureOfContentTermsClient { + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + NatureOfContentTerm getById(@PathVariable String id); +} diff --git a/src/main/java/org/folio/dew/controller/BulkEditController.java b/src/main/java/org/folio/dew/controller/BulkEditController.java index fe799da5b..b16e0fd3e 100644 --- a/src/main/java/org/folio/dew/controller/BulkEditController.java +++ b/src/main/java/org/folio/dew/controller/BulkEditController.java @@ -8,6 +8,7 @@ import static org.folio.dew.domain.dto.EntityType.HOLDINGS_RECORD; import static org.folio.dew.domain.dto.EntityType.ITEM; import static org.folio.dew.domain.dto.EntityType.USER; +import static org.folio.dew.domain.dto.EntityType.INSTANCE; import static org.folio.dew.domain.dto.ExportType.BULK_EDIT_IDENTIFIERS; import static org.folio.dew.domain.dto.ExportType.BULK_EDIT_UPDATE; import static org.folio.dew.domain.dto.JobParameterNames.PREVIEW_FILE_NAME; @@ -65,6 +66,7 @@ import org.folio.dew.domain.dto.HoldingsFormat; import org.folio.dew.domain.dto.HoldingsRecordCollection; import org.folio.dew.domain.dto.IdentifierType; +import org.folio.dew.domain.dto.InstanceFormat; import org.folio.dew.domain.dto.ItemCollection; import org.folio.dew.domain.dto.ItemContentUpdateCollection; import org.folio.dew.domain.dto.ItemFormat; @@ -569,6 +571,8 @@ private int getIdentifierIndex(JobCommand jobCommand) { return Arrays.asList(ItemFormat.getItemFieldsArray()).indexOf(resolveIdentifier(jobCommand.getIdentifierType().getValue())); } else if (HOLDINGS_RECORD == jobCommand.getEntityType()) { return Arrays.asList(HoldingsFormat.getHoldingsFieldsArray()).indexOf(IdentifierType.ID.getValue().toLowerCase()); + }else if (INSTANCE == jobCommand.getEntityType()) { + return Arrays.asList(InstanceFormat.getInstanceFieldsArray()).indexOf(IdentifierType.ID.getValue().toLowerCase()); } else { throw new NonSupportedEntityException(format("Non-supported entity type: %s", jobCommand.getEntityType())); } diff --git a/src/main/java/org/folio/dew/domain/dto/InstanceFormat.java b/src/main/java/org/folio/dew/domain/dto/InstanceFormat.java new file mode 100644 index 000000000..8eec668e2 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/InstanceFormat.java @@ -0,0 +1,124 @@ +package org.folio.dew.domain.dto; + + +import com.opencsv.bean.CsvBindByName; +import com.opencsv.bean.CsvBindByPosition; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.With; +import org.apache.commons.lang3.reflect.FieldUtils; + +import java.lang.reflect.Field; +import java.util.stream.Collectors; +@Data +@Builder +@With +@NoArgsConstructor +@AllArgsConstructor +public class InstanceFormat implements Formatable { + + private org.folio.dew.domain.dto.Instance original; + + @CsvBindByName(column = "Instance UUID") + @CsvBindByPosition(position = 0) + private String id; + + @CsvBindByName(column = "Suppress from discovery") + @CsvBindByPosition(position = 1) + private String discoverySuppress; + + @CsvBindByName(column = "Staff suppress") + @CsvBindByPosition(position = 2) + private String staffSuppress; + + @CsvBindByName(column = "Previously held") + @CsvBindByPosition(position = 3) + private String previouslyHeld; + + @CsvBindByName(column = "Instance HRID") + @CsvBindByPosition(position = 4) + private String hrid; + + @CsvBindByName(column = "Source") + @CsvBindByPosition(position = 5) + private String source; + + @CsvBindByName(column = "Cataloged date") + @CsvBindByPosition(position = 6) + private String catalogedDate; + + @CsvBindByName(column = "Instance status term") + @CsvBindByPosition(position = 7) + private String statusId; + + @CsvBindByName(column = "Mode of issuance") + @CsvBindByPosition(position = 8) + private String modeOfIssuanceId; + + @CsvBindByName(column = "Administrative note") + @CsvBindByPosition(position = 9) + private String administrativeNotes; + + @CsvBindByName(column = "Resource title") + @CsvBindByPosition(position = 10) + private String title; + + @CsvBindByName(column = "Index title") + @CsvBindByPosition(position = 11) + private String indexTitle; + + @CsvBindByName(column = "Series statements") + @CsvBindByPosition(position = 12) + private String series; + + @CsvBindByName(column = "Contributors") + @CsvBindByPosition(position = 13) + private String contributors; + + @CsvBindByName(column = "Edition") + @CsvBindByPosition(position = 14) + private String editions; + + @CsvBindByName(column = "Physical description") + @CsvBindByPosition(position = 15) + private String physicalDescriptions; + + @CsvBindByName(column = "Resource type") + @CsvBindByPosition(position = 16) + private String instanceTypeId; + + @CsvBindByName(column = "Nature of content") + @CsvBindByPosition(position = 17) + private String natureOfContentTermIds; + + @CsvBindByName(column = "Formats") + @CsvBindByPosition(position = 18) + private String instanceFormatIds; + + @CsvBindByName(column = "Languages") + @CsvBindByPosition(position = 19) + private String languages; + + @CsvBindByName(column = "Publication frequency") + @CsvBindByPosition(position = 20) + private String publicationFrequency; + + @CsvBindByName(column = "Publication range") + @CsvBindByPosition(position = 21) + private String publicationRange; + + public static String[] getInstanceFieldsArray() { + return FieldUtils.getFieldsListWithAnnotation(InstanceFormat.class, CsvBindByName.class).stream() + .map(Field::getName) + .toArray(String[]::new); + } + + public static String getInstanceColumnHeaders() { + return FieldUtils.getFieldsListWithAnnotation(InstanceFormat.class, CsvBindByName.class).stream() + .map(field -> field.getAnnotation(CsvBindByName.class).column()) + .collect(Collectors.joining(",")); + } + +} diff --git a/src/main/java/org/folio/dew/service/InstanceReferenceService.java b/src/main/java/org/folio/dew/service/InstanceReferenceService.java new file mode 100644 index 000000000..91e14d2e2 --- /dev/null +++ b/src/main/java/org/folio/dew/service/InstanceReferenceService.java @@ -0,0 +1,75 @@ +package org.folio.dew.service; + + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.dew.client.*; +import org.folio.dew.domain.dto.ErrorServiceArgs; +import org.folio.dew.error.BulkEditException; +import org.folio.dew.error.NotFoundException; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import static org.apache.commons.lang3.ObjectUtils.isEmpty; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +@Service +@Log4j2 +@RequiredArgsConstructor +public class InstanceReferenceService { + + private final BulkEditProcessingErrorsService errorsService; + private final InstanceStatusesClient instanceStatusesClient; + private final InstanceModeOfIssuanceClient instanceModeOfIssuanceClient; + private final InstanceTypesClient instanceTypesClient; + private final NatureOfContentTermsClient natureOfContentTermsClient; + private final InstanceFormatsClient instanceFormatsClient; + + + @Cacheable(cacheNames = "instanceStatusNames") + public String getInstanceStatusNameById(String instanceStatusId, ErrorServiceArgs args) { + try { + return isEmpty(instanceStatusId) ? EMPTY : instanceStatusesClient.getById(instanceStatusId).getName(); + } catch (NotFoundException e) { + errorsService.saveErrorInCSV(args.getJobId(), args.getIdentifier(), new BulkEditException(String.format("Instance status was not found by id: [%s]", instanceStatusId)), args.getFileName()); + return instanceStatusId; + } + } + @Cacheable(cacheNames = "issuanceModeNames") + public String getModeOfIssuanceNameById(String issuanceModeId, ErrorServiceArgs args) { + try { + return isEmpty(issuanceModeId) ? EMPTY : instanceModeOfIssuanceClient.getById(issuanceModeId).getName(); + } catch (NotFoundException e) { + errorsService.saveErrorInCSV(args.getJobId(), args.getIdentifier(), new BulkEditException(String.format("Issuance mode was not found by id: [%s]", issuanceModeId)), args.getFileName()); + return issuanceModeId; + } + } + @Cacheable(cacheNames = "instanceTypes") + public String getInstanceTypeNameById(String instanceTypeId, ErrorServiceArgs args) { + try { + return isEmpty(instanceTypeId) ? EMPTY : instanceTypesClient.getById(instanceTypeId).getName(); + } catch (NotFoundException e) { + errorsService.saveErrorInCSV(args.getJobId(), args.getIdentifier(), new BulkEditException(String.format("Instance type was not found by id: [%s]", instanceTypeId)), args.getFileName()); + return instanceTypeId; + } + } + @Cacheable(cacheNames = "natureOfContentTermIds") + public String getNatureOfContentTermNameById(String natureOfContentTermId, ErrorServiceArgs args) { + try { + return isEmpty(natureOfContentTermId) ? EMPTY : natureOfContentTermsClient.getById(natureOfContentTermId).getName(); + } catch (NotFoundException e) { + errorsService.saveErrorInCSV(args.getJobId(), args.getIdentifier(), new BulkEditException(String.format("Nature of content term was not found by id: [%s]", natureOfContentTermId)), args.getFileName()); + return natureOfContentTermId; + } + } + @Cacheable(cacheNames = "instanceFormatIds") + public String getFormatOfInstanceNameById(String instanceFormatId, ErrorServiceArgs args) { + try { + return isEmpty(instanceFormatId) ? EMPTY : instanceFormatsClient.getById(instanceFormatId).getName(); + } catch (NotFoundException e) { + errorsService.saveErrorInCSV(args.getJobId(), args.getIdentifier(), new BulkEditException(String.format("Instance format was not found by id: [%s]", instanceFormatId)), args.getFileName()); + return instanceFormatId; + } + } + +} diff --git a/src/main/resources/swagger.api/bulk-edit.yaml b/src/main/resources/swagger.api/bulk-edit.yaml index 4f1a9b19c..60f4d53b6 100644 --- a/src/main/resources/swagger.api/bulk-edit.yaml +++ b/src/main/resources/swagger.api/bulk-edit.yaml @@ -659,6 +659,24 @@ components: $ref: '../../../../folio-export-common/schemas/inventory/holdingsRecordsSourceCollection.json#/HoldingsRecordsSourceCollection' holdingsContentUpdateCollection: $ref: '../../../../folio-export-common/schemas/bulk-edit/holdingsContentUpdateCollection.json#/HoldingsContentUpdateCollection' + instance: + $ref: '../../../../folio-export-common/schemas/inventory-storage/instance.json#/Instance' + instanceCollection: + $ref: '../../../../folio-export-common/schemas/inventory-storage/instanceCollection.json#/InstanceCollection' + formatOfInstance: + $ref: '../../../../folio-export-common/schemas/inventory-storage/formatOfInstance.json#/FormatOfInstance' + classificationType: + $ref: '../../../../folio-export-common/schemas/inventory-storage/classificationType.json#/ClassificationType' + contributorNameType: + $ref: '../../../../folio-export-common/schemas/inventory-storage/contributorNameType.json#/ContributorNameType' + instanceStatus: + $ref: '../../../../folio-export-common/schemas/inventory/instanceStatus.json#/InstanceStatus' + issuanceMode: + $ref: '../../../../folio-export-common/schemas/inventory/issuanceMode.json#/IssuanceMode' + instanceType: + $ref: '../../../../folio-export-common/schemas/inventory/instanceType.json#/InstanceType' + natureOfContentTerm: + $ref: '../../../../folio-export-common/schemas/inventory/natureOfContentTerm.json#/NatureOfContentTerm' examples: errors: value: From e7c875edd08f3f5b21502c3f58b412811852d732 Mon Sep 17 00:00:00 2001 From: Dmytro_Bykov Date: Fri, 29 Dec 2023 18:20:37 +0500 Subject: [PATCH 2/3] MODEXPW-452: Retrieve instance records for bulk edit. git feature --- .gitmodules | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitmodules b/.gitmodules index 214ef69b3..2bea98d58 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "folio-export-common"] path = folio-export-common url = https://github.com/folio-org/folio-export-common.git + update = merge From 8f79e339490add9c31183808fb2a561e6aaab7fe Mon Sep 17 00:00:00 2001 From: Dmytro_Bykov Date: Fri, 29 Dec 2023 21:05:53 +0500 Subject: [PATCH 3/3] MODEXPW-452: submodule reference update --- folio-export-common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/folio-export-common b/folio-export-common index e3fe67a1d..5d5a0cec9 160000 --- a/folio-export-common +++ b/folio-export-common @@ -1 +1 @@ -Subproject commit e3fe67a1dd9fe0aba1e95b9b326791d051b98ecd +Subproject commit 5d5a0cec9f56152a24cf0b62b95bd2707eaaeb21