Skip to content

Commit

Permalink
MODEXPW-452: Retrieve instance records for bulk edit. Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
DmytroBykov1 committed Dec 29, 2023
1 parent 3e0c51d commit 197a676
Show file tree
Hide file tree
Showing 15 changed files with 623 additions and 1 deletion.
7 changes: 6 additions & 1 deletion descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<InstanceCollection, List<InstanceFormat>> {
private final BulkEditInstanceProcessor bulkEditInstanceProcessor;

@Override
public List<InstanceFormat> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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<Instance, InstanceFormat> {

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<FormatOfInstance> 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<String> 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<InstanceContributorsInner> 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<InstanceSeriesInner> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ItemIdentifier> csvItemIdentifierReader,
CompositeItemWriter<List<InstanceFormat>> compositeInstanceListWriter,
ListIdentifiersWriteListener<InstanceFormat> listIdentifiersWriteListener, JobRepository jobRepository,
PlatformTransactionManager transactionManager) {
return new StepBuilder("bulkEditInstanceStep", jobRepository)
.<ItemIdentifier, List<InstanceFormat>> 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<ItemIdentifier, List<InstanceFormat>> identifierInstanceProcessor() {
var processor = new CompositeItemProcessor<ItemIdentifier, List<InstanceFormat>>();
processor.setDelegates(Arrays.asList(instanceFetcher, bulkEditInstanceListProcessor));
return processor;
}

@Bean
@StepScope
public CompositeItemWriter<List<InstanceFormat>> compositeInstanceListWriter(@Value("#{jobParameters['" + TEMP_LOCAL_FILE_PATH + "']}") String outputFileName) {
var writer = new CompositeItemWriter<List<InstanceFormat>>();
writer.setDelegates(Arrays.asList(new CsvListFileWriter<>(outputFileName, InstanceFormat.getInstanceColumnHeaders(), InstanceFormat.getInstanceFieldsArray(), (field, i) -> field),
new JsonListFileWriter<>(new FileSystemResource(outputFileName + ".json"))));
return writer;
}
}
Original file line number Diff line number Diff line change
@@ -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<ItemIdentifier, InstanceCollection> {
private final InventoryInstancesClient inventoryInstancesClient;

@Value("#{jobParameters['identifierType']}")
private String identifierType;

private final Set<ItemIdentifier> 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));
}
}
}
14 changes: 14 additions & 0 deletions src/main/java/org/folio/dew/client/InstanceFormatsClient.java
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
14 changes: 14 additions & 0 deletions src/main/java/org/folio/dew/client/InstanceStatusesClient.java
Original file line number Diff line number Diff line change
@@ -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);
}
13 changes: 13 additions & 0 deletions src/main/java/org/folio/dew/client/InstanceTypesClient.java
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit 197a676

Please sign in to comment.