diff --git a/NEWS.md b/NEWS.md index 1a91f07c..211f2c77 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ ## v8.0.0 YYYY-mm-DD ### Breaking changes * Update QuickMARC to use generation field from SRS for optimistic locking ([MODQM-478](https://folio-org.atlassian.net/browse/MODQM-478)) +* Upgrade module to SpringBoot 4.0.x and Spring 7.0.x ([MODQM-487](https://folio-org.atlassian.net/browse/MODQM-487)) ### New APIs versions * Provides `API_NAME vX.Y` diff --git a/pom.xml b/pom.xml index 5a1c0e50..cd7648bb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.7 + 4.0.2 @@ -23,7 +23,7 @@ ${project.basedir}/src/main/resources/swagger.api/records-editor-async.yaml ${project.basedir}/src/main/resources/swagger.api/marc-specifications.yaml - 10.0.0-SNAPSHOT + 10.0.0-RC1 5.1.0-SNAPSHOT 2.1.0-SNAPSHOT 2.9.6 @@ -39,7 +39,7 @@ 3.5.4 3.3.1 3.6.0 - 12.1.2 + 13.0.0 1.2.0 @@ -85,18 +85,13 @@ - org.springframework.kafka - spring-kafka - - - - com.fasterxml.jackson.module - jackson-module-jaxb-annotations + org.springframework.boot + spring-boot-starter-kafka - org.hibernate.validator - hibernate-validator + org.springframework.boot + spring-boot-starter-validation @@ -141,6 +136,11 @@ + + org.springframework.boot + spring-boot-starter-webmvc-test + + org.folio folio-spring-testing diff --git a/src/main/java/org/folio/qm/ModQuickMarcApplication.java b/src/main/java/org/folio/qm/ModQuickMarcApplication.java index aef114f8..625026c2 100644 --- a/src/main/java/org/folio/qm/ModQuickMarcApplication.java +++ b/src/main/java/org/folio/qm/ModQuickMarcApplication.java @@ -2,12 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling -@EnableFeignClients public class ModQuickMarcApplication { public static void main(String[] args) { diff --git a/src/main/java/org/folio/qm/client/ChangeManagerClient.java b/src/main/java/org/folio/qm/client/ChangeManagerClient.java index 40bb4985..8fdef5c8 100644 --- a/src/main/java/org/folio/qm/client/ChangeManagerClient.java +++ b/src/main/java/org/folio/qm/client/ChangeManagerClient.java @@ -6,28 +6,27 @@ import org.folio.qm.client.model.ParsedRecordDto; import org.folio.qm.client.model.ProfileInfo; import org.folio.qm.client.model.RawRecordsDto; -import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.service.annotation.HttpExchange; +import org.springframework.web.service.annotation.PostExchange; +import org.springframework.web.service.annotation.PutExchange; -@FeignClient(value = "change-manager") +@HttpExchange(url = "change-manager", contentType = MediaType.APPLICATION_JSON_VALUE) public interface ChangeManagerClient { - @PutMapping(value = "/parsedRecords/{id}") - void putParsedRecordByInstanceId(@PathVariable("id") UUID id, ParsedRecordDto recordDto); + @PutExchange(value = "/parsedRecords/{id}") + void putParsedRecordByInstanceId(@PathVariable("id") UUID id, @RequestBody ParsedRecordDto recordDto); - @PostMapping(value = "/jobExecutions", produces = MediaType.APPLICATION_JSON_VALUE) + @PostExchange(value = "/jobExecutions") InitJobExecutionsRsDto postJobExecution(@RequestBody InitJobExecutionsRqDto jobExecutionDto); - @PutMapping(value = "/jobExecutions/{jobExecutionId}/jobProfile", produces = MediaType.APPLICATION_JSON_VALUE) + @PutExchange(value = "/jobExecutions/{jobExecutionId}/jobProfile") void putJobProfileByJobExecutionId(@PathVariable("jobExecutionId") UUID jobExecutionId, @RequestBody ProfileInfo jobProfile); - @PostMapping(value = "/jobExecutions/{jobExecutionId}/records", - produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE}) + @PostExchange(value = "/jobExecutions/{jobExecutionId}/records") void postRawRecordsByJobExecutionId(@PathVariable("jobExecutionId") UUID jobExecutionId, @RequestBody RawRecordsDto rawRecords); } diff --git a/src/main/java/org/folio/qm/client/FieldProtectionSettingsClient.java b/src/main/java/org/folio/qm/client/FieldProtectionSettingsClient.java index 78c68548..09821097 100644 --- a/src/main/java/org/folio/qm/client/FieldProtectionSettingsClient.java +++ b/src/main/java/org/folio/qm/client/FieldProtectionSettingsClient.java @@ -1,13 +1,13 @@ package org.folio.qm.client; import org.folio.qm.client.model.MarcFieldProtectionSettingsCollection; -import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; -@FeignClient(value = "field-protection-settings") +@HttpExchange(url = "field-protection-settings", accept = MediaType.APPLICATION_JSON_VALUE) public interface FieldProtectionSettingsClient { - @GetMapping(value = "/marc?limit=1000", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE}) + @GetExchange(value = "/marc?limit=1000") MarcFieldProtectionSettingsCollection getFieldProtectionSettings(); } diff --git a/src/main/java/org/folio/qm/client/LinkingRulesClient.java b/src/main/java/org/folio/qm/client/LinkingRulesClient.java index 8d1864ac..981e36d3 100644 --- a/src/main/java/org/folio/qm/client/LinkingRulesClient.java +++ b/src/main/java/org/folio/qm/client/LinkingRulesClient.java @@ -3,14 +3,14 @@ import java.util.List; import lombok.Data; import lombok.experimental.Accessors; -import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; -@FeignClient(value = "linking-rules", dismiss404 = true) +@HttpExchange(url = "linking-rules", accept = MediaType.APPLICATION_JSON_VALUE) public interface LinkingRulesClient { - @GetMapping(value = "instance-authority", produces = MediaType.APPLICATION_JSON_VALUE) + @GetExchange(value = "instance-authority") List fetchLinkingRules(); @Data diff --git a/src/main/java/org/folio/qm/client/LinksClient.java b/src/main/java/org/folio/qm/client/LinksClient.java index 20368b99..6f318273 100644 --- a/src/main/java/org/folio/qm/client/LinksClient.java +++ b/src/main/java/org/folio/qm/client/LinksClient.java @@ -7,20 +7,21 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; -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.RequestBody; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; +import org.springframework.web.service.annotation.PutExchange; -@FeignClient(value = "links", dismiss404 = true) +@HttpExchange(url = "links", contentType = MediaType.APPLICATION_JSON_VALUE) public interface LinksClient { - @GetMapping(value = "/instances/{instanceId}", produces = MediaType.APPLICATION_JSON_VALUE) + @GetExchange(value = "/instances/{instanceId}") Optional fetchLinksByInstanceId(@PathVariable("instanceId") UUID instanceId); - @PutMapping("/instances/{instanceId}") - void putLinksByInstanceId(@PathVariable("instanceId") UUID instanceId, InstanceLinks instanceLinks); + @PutExchange("/instances/{instanceId}") + void putLinksByInstanceId(@PathVariable("instanceId") UUID instanceId, @RequestBody InstanceLinks instanceLinks); record InstanceLinks(List links, Integer totalRecords) { } diff --git a/src/main/java/org/folio/qm/client/LinksSuggestionsClient.java b/src/main/java/org/folio/qm/client/LinksSuggestionsClient.java index a6271213..9e258bcb 100644 --- a/src/main/java/org/folio/qm/client/LinksSuggestionsClient.java +++ b/src/main/java/org/folio/qm/client/LinksSuggestionsClient.java @@ -2,14 +2,14 @@ import org.folio.qm.client.model.EntitiesLinksSuggestions; import org.folio.qm.domain.dto.AuthoritySearchParameter; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.HttpExchange; -@FeignClient(value = "links-suggestions", dismiss404 = true) +@HttpExchange(url = "links-suggestions", accept = MediaType.APPLICATION_JSON_VALUE) public interface LinksSuggestionsClient { - @PostMapping("/marc") + @HttpExchange("/marc") EntitiesLinksSuggestions postLinksSuggestions(EntitiesLinksSuggestions srsMarcRecord, @RequestParam AuthoritySearchParameter authoritySearchParameter, @RequestParam Boolean ignoreAutoLinkingEnabled); diff --git a/src/main/java/org/folio/qm/client/SourceStorageClient.java b/src/main/java/org/folio/qm/client/SourceStorageClient.java index b0081382..93f38a06 100644 --- a/src/main/java/org/folio/qm/client/SourceStorageClient.java +++ b/src/main/java/org/folio/qm/client/SourceStorageClient.java @@ -1,17 +1,18 @@ package org.folio.qm.client; +import java.util.Optional; import org.folio.qm.client.model.SourceRecord; -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.RequestParam; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; -@FeignClient(value = "source-storage") +@HttpExchange(url = "source-storage", accept = MediaType.APPLICATION_JSON_VALUE) public interface SourceStorageClient { - @GetMapping(value = "/source-records/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - SourceRecord getSourceRecord(@PathVariable("id") String id, @RequestParam("idType") IdType idType); + @GetExchange(value = "/source-records/{id}") + Optional getSourceRecord(@PathVariable("id") String id, @RequestParam("idType") IdType idType); enum IdType { EXTERNAL diff --git a/src/main/java/org/folio/qm/client/SpecificationStorageClient.java b/src/main/java/org/folio/qm/client/SpecificationStorageClient.java index 0f6fc3c8..63cc6b31 100644 --- a/src/main/java/org/folio/qm/client/SpecificationStorageClient.java +++ b/src/main/java/org/folio/qm/client/SpecificationStorageClient.java @@ -3,20 +3,18 @@ import java.util.UUID; import org.folio.rspec.domain.dto.SpecificationDto; import org.folio.rspec.domain.dto.SpecificationDtoCollection; -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.RequestParam; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; -@FeignClient(value = "specification-storage") +@HttpExchange(url = "specification-storage", contentType = MediaType.APPLICATION_JSON_VALUE) public interface SpecificationStorageClient { - @GetMapping(value = "/specifications?family=MARC&include=all&limit=1", - produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE}) + @GetExchange(value = "/specifications?family=MARC&include=all&limit=1") SpecificationDtoCollection getSpecifications(@RequestParam("profile") String profile); - @GetMapping(value = "/specifications/{specificationId}?include=all", - produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE}) + @GetExchange(value = "/specifications/{specificationId}?include=all") SpecificationDto getSpecification(@PathVariable("specificationId") UUID specificationId); } diff --git a/src/main/java/org/folio/qm/client/UsersClient.java b/src/main/java/org/folio/qm/client/UsersClient.java index 0ea2fc1d..c41f2841 100644 --- a/src/main/java/org/folio/qm/client/UsersClient.java +++ b/src/main/java/org/folio/qm/client/UsersClient.java @@ -1,15 +1,15 @@ package org.folio.qm.client; import java.util.Optional; -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.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; -@FeignClient(value = "users", dismiss404 = true) +@HttpExchange(url = "users", accept = MediaType.APPLICATION_JSON_VALUE) public interface UsersClient { - @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + @GetExchange(value = "/{id}") Optional fetchUserById(@PathVariable("id") String id); record UserDto(String id, String username, UserPersonal personal) { diff --git a/src/main/java/org/folio/qm/config/CacheConfig.java b/src/main/java/org/folio/qm/config/CacheConfig.java index eaa0a0f5..7524bdb9 100644 --- a/src/main/java/org/folio/qm/config/CacheConfig.java +++ b/src/main/java/org/folio/qm/config/CacheConfig.java @@ -5,7 +5,7 @@ import java.util.ArrayList; import java.util.Collection; import org.folio.qm.config.properties.CustomCacheProperties; -import org.springframework.boot.autoconfigure.cache.CacheProperties; +import org.springframework.boot.cache.autoconfigure.CacheProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; diff --git a/src/main/java/org/folio/qm/config/HttpClientConfiguration.java b/src/main/java/org/folio/qm/config/HttpClientConfiguration.java new file mode 100644 index 00000000..306c4ec8 --- /dev/null +++ b/src/main/java/org/folio/qm/config/HttpClientConfiguration.java @@ -0,0 +1,57 @@ +package org.folio.qm.config; + +import org.folio.qm.client.ChangeManagerClient; +import org.folio.qm.client.FieldProtectionSettingsClient; +import org.folio.qm.client.LinkingRulesClient; +import org.folio.qm.client.LinksClient; +import org.folio.qm.client.LinksSuggestionsClient; +import org.folio.qm.client.SourceStorageClient; +import org.folio.qm.client.SpecificationStorageClient; +import org.folio.qm.client.UsersClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +@Configuration +public class HttpClientConfiguration { + + @Bean + public ChangeManagerClient changeManagerClient(HttpServiceProxyFactory factory) { + return factory.createClient(ChangeManagerClient.class); + } + + @Bean + public FieldProtectionSettingsClient fieldProtectionSettingsClient(HttpServiceProxyFactory factory) { + return factory.createClient(FieldProtectionSettingsClient.class); + } + + @Bean + public LinkingRulesClient linkingRulesClient(HttpServiceProxyFactory factory) { + return factory.createClient(LinkingRulesClient.class); + } + + @Bean + public LinksClient linksClient(HttpServiceProxyFactory factory) { + return factory.createClient(LinksClient.class); + } + + @Bean + public LinksSuggestionsClient linksSuggestionsClient(HttpServiceProxyFactory factory) { + return factory.createClient(LinksSuggestionsClient.class); + } + + @Bean + public SourceStorageClient sourceStorageClient(HttpServiceProxyFactory factory) { + return factory.createClient(SourceStorageClient.class); + } + + @Bean + public SpecificationStorageClient specificationStorageClient(HttpServiceProxyFactory factory) { + return factory.createClient(SpecificationStorageClient.class); + } + + @Bean + public UsersClient usersClient(HttpServiceProxyFactory factory) { + return factory.createClient(UsersClient.class); + } +} diff --git a/src/main/java/org/folio/qm/config/KafkaConfig.java b/src/main/java/org/folio/qm/config/KafkaConfig.java index aae4d190..b53824e0 100644 --- a/src/main/java/org/folio/qm/config/KafkaConfig.java +++ b/src/main/java/org/folio/qm/config/KafkaConfig.java @@ -11,14 +11,14 @@ import org.folio.qm.messaging.domain.QmCompletedEventPayload; import org.folio.rspec.domain.dto.SpecificationUpdatedEvent; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; +import org.springframework.boot.kafka.autoconfigure.KafkaProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.kafka.annotation.EnableKafka; import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; import org.springframework.kafka.core.ConsumerFactory; import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.support.serializer.JsonDeserializer; +import org.springframework.kafka.support.serializer.JacksonJsonDeserializer; @Configuration @EnableKafka @@ -33,7 +33,7 @@ public String kafkaEnvId(@Value("${ENV:folio}") String envId) { public ConsumerFactory dataImportConsumerFactory(KafkaProperties kafkaProperties, Deserializer deserializer) { - Map consumerProperties = new HashMap<>(kafkaProperties.buildConsumerProperties(null)); + Map consumerProperties = new HashMap<>(kafkaProperties.buildConsumerProperties()); consumerProperties.put(KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); consumerProperties.put(VALUE_DESERIALIZER_CLASS_CONFIG, deserializer); return new DefaultKafkaConsumerFactory<>(consumerProperties, new StringDeserializer(), deserializer); @@ -52,7 +52,7 @@ public ConsumerFactory dataImportConsumerFactory public ConsumerFactory quickMarcConsumerFactory(KafkaProperties kafkaProperties, Deserializer deserializer) { - Map consumerProperties = new HashMap<>(kafkaProperties.buildConsumerProperties(null)); + Map consumerProperties = new HashMap<>(kafkaProperties.buildConsumerProperties()); consumerProperties.put(KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); consumerProperties.put(VALUE_DESERIALIZER_CLASS_CONFIG, deserializer); return new DefaultKafkaConsumerFactory<>(consumerProperties, new StringDeserializer(), deserializer); @@ -70,8 +70,8 @@ public ConsumerFactory quickMarcConsumerFactory @Bean public ConsumerFactory specificationUpdatedConsumerFactory( KafkaProperties kafkaProperties) { - var deserializer = new JsonDeserializer<>(SpecificationUpdatedEvent.class, false); - Map consumerProperties = new HashMap<>(kafkaProperties.buildConsumerProperties(null)); + var deserializer = new JacksonJsonDeserializer<>(SpecificationUpdatedEvent.class, false); + Map consumerProperties = new HashMap<>(kafkaProperties.buildConsumerProperties()); consumerProperties.put(KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); consumerProperties.put(VALUE_DESERIALIZER_CLASS_CONFIG, deserializer); return new DefaultKafkaConsumerFactory<>(consumerProperties, new StringDeserializer(), deserializer); diff --git a/src/main/java/org/folio/qm/controller/ErrorHandling.java b/src/main/java/org/folio/qm/controller/ErrorHandling.java index 72cb293d..613b8b15 100644 --- a/src/main/java/org/folio/qm/controller/ErrorHandling.java +++ b/src/main/java/org/folio/qm/controller/ErrorHandling.java @@ -1,13 +1,11 @@ package org.folio.qm.controller; -import static feign.Util.UTF_8; import static org.folio.qm.util.ErrorUtils.ErrorType.FOLIO_EXTERNAL_OR_UNDEFINED; import static org.folio.qm.util.ErrorUtils.ErrorType.INTERNAL; import static org.folio.qm.util.ErrorUtils.ErrorType.UNKNOWN; import static org.folio.qm.util.ErrorUtils.buildError; import static org.folio.qm.util.ErrorUtils.buildErrors; -import feign.FeignException; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.ConstraintViolationException; import lombok.extern.log4j.Log4j2; @@ -30,6 +28,8 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.ResourceAccessException; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; @@ -42,21 +42,18 @@ public class ErrorHandling { private static final String ARGUMENT_NOT_VALID_MSG_PATTERN = "Parameter '%s' %s"; private static final String CONSTRAINT_VIOLATION_MSG_PATTERN = "Parameter %s"; - @ExceptionHandler(FeignException.class) - public Error handleFeignStatusException(FeignException e, HttpServletResponse response) { - var status = e.status(); - if (status != -1) { - var message = e.responseBody() - .map(byteBuffer -> new String(byteBuffer.array(), UTF_8)) - .orElse(e.getMessage()); - response.setStatus(status); - log.warn(message); - return buildErrors(status, FOLIO_EXTERNAL_OR_UNDEFINED, message); - } else { - log.warn(e.getMessage()); - response.setStatus(HttpStatus.BAD_REQUEST.value()); - return buildError(HttpStatus.BAD_REQUEST, FOLIO_EXTERNAL_OR_UNDEFINED, e.getMessage()); + @ExceptionHandler(HttpStatusCodeException.class) + public Error handleHttpStatusException(HttpStatusCodeException e, HttpServletResponse response) { + var status = e.getStatusCode().value(); + var message = e.getResponseBodyAsString(); + + if (message.isEmpty()) { + message = e.getMessage(); } + + response.setStatus(status); + log.warn(message); + return buildErrors(status, FOLIO_EXTERNAL_OR_UNDEFINED, message); } @ExceptionHandler(MethodArgumentNotValidException.class) @@ -73,6 +70,13 @@ public Error handleMethodArgumentNotValidException(MethodArgumentNotValidExcepti } } + @ExceptionHandler(ResourceAccessException.class) + @ResponseStatus(value = HttpStatus.BAD_REQUEST) + public Error handleResourceAccessException(ResourceAccessException e) { + log.warn(e.getMessage()); + return buildError(HttpStatus.BAD_REQUEST, FOLIO_EXTERNAL_OR_UNDEFINED, e.getMessage()); + } + @ExceptionHandler(QuickMarcException.class) public Error handleQuickMarcException(QuickMarcException e, HttpServletResponse response) { log.warn(e); @@ -110,14 +114,14 @@ public Error handleNotFoundException(NotFoundException e) { } @ExceptionHandler(FieldsValidationException.class) - @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_CONTENT) public Object handleFieldsValidationException(FieldsValidationException e) { var errors = e.getValidationResult().errors(); return errors.size() == 1 ? buildError(errors.getFirst()) : buildErrors(errors); } @ExceptionHandler(MarcRecordValidationException.class) - @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_CONTENT) public ValidationResult handleMarcRecordValidationException(MarcRecordValidationException e) { log.warn("Marc record validation error occurred: {}", e.getMessage()); return e.getValidationResult(); diff --git a/src/main/java/org/folio/qm/controller/filter/UserIdOkapiHeaderValidationFilter.java b/src/main/java/org/folio/qm/controller/filter/UserIdOkapiHeaderValidationFilter.java index 90c0f1f1..28897825 100644 --- a/src/main/java/org/folio/qm/controller/filter/UserIdOkapiHeaderValidationFilter.java +++ b/src/main/java/org/folio/qm/controller/filter/UserIdOkapiHeaderValidationFilter.java @@ -12,7 +12,7 @@ import lombok.Setter; import org.folio.spring.integration.XOkapiHeaders; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.servlet.filter.OrderedFilter; +import org.springframework.boot.servlet.filter.OrderedFilter; import org.springframework.stereotype.Component; import org.springframework.util.MimeTypeUtils; import org.springframework.web.filter.GenericFilterBean; diff --git a/src/main/java/org/folio/qm/converter/MarcQmConverter.java b/src/main/java/org/folio/qm/converter/MarcQmConverter.java index 91b7c5d1..0666e800 100644 --- a/src/main/java/org/folio/qm/converter/MarcQmConverter.java +++ b/src/main/java/org/folio/qm/converter/MarcQmConverter.java @@ -4,9 +4,6 @@ import static org.folio.qm.util.MarcUtils.encodeToMarcDateTime; import static org.folio.qm.util.MarcUtils.getFieldByTag; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; import java.io.ByteArrayOutputStream; import java.time.LocalDateTime; import java.util.HashMap; @@ -23,10 +20,13 @@ import org.folio.qm.exception.ConverterException; import org.folio.qm.mapper.MarcTypeMapper; import org.folio.qm.util.QmMarcJsonWriter; +import org.jspecify.annotations.NonNull; import org.marc4j.marc.Record; import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ArrayNode; @Component @RequiredArgsConstructor @@ -74,7 +74,7 @@ private void reorderContentTagsBasedOnSource(JsonNode convertedContent, List> jsonNodesByTag = new HashMap<>(); fieldsArrayNode.forEach(node -> { - String tag = node.fieldNames().next(); + String tag = node.propertyNames().iterator().next(); jsonNodesByTag.computeIfAbsent(tag, k -> new LinkedList<>()).add(node); }); diff --git a/src/main/java/org/folio/qm/converter/MarcQmCreateConverter.java b/src/main/java/org/folio/qm/converter/MarcQmCreateConverter.java index d35005fb..62c359b3 100644 --- a/src/main/java/org/folio/qm/converter/MarcQmCreateConverter.java +++ b/src/main/java/org/folio/qm/converter/MarcQmCreateConverter.java @@ -1,14 +1,14 @@ package org.folio.qm.converter; -import com.fasterxml.jackson.databind.ObjectMapper; import org.folio.qm.client.model.ParsedRecordDto; import org.folio.qm.domain.dto.BaseMarcRecord; import org.folio.qm.domain.dto.QuickMarcCreate; import org.folio.qm.mapper.MarcTypeMapper; +import org.jspecify.annotations.NonNull; import org.marc4j.marc.Record; import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; +import tools.jackson.databind.ObjectMapper; @Component public class MarcQmCreateConverter extends MarcQmConverter { diff --git a/src/main/java/org/folio/qm/converter/MarcQmEditConverter.java b/src/main/java/org/folio/qm/converter/MarcQmEditConverter.java index af3dc8f9..cd5ccfb2 100644 --- a/src/main/java/org/folio/qm/converter/MarcQmEditConverter.java +++ b/src/main/java/org/folio/qm/converter/MarcQmEditConverter.java @@ -3,7 +3,6 @@ import static org.folio.qm.util.ErrorCodes.ILLEGAL_MARC_FORMAT; import static org.folio.qm.util.ErrorUtils.buildInternalError; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Objects; import org.folio.qm.client.model.ExternalIdsHolder; import org.folio.qm.client.model.ParsedRecordDto; @@ -11,10 +10,11 @@ import org.folio.qm.domain.dto.QuickMarcEdit; import org.folio.qm.exception.ConverterException; import org.folio.qm.mapper.MarcTypeMapper; +import org.jspecify.annotations.NonNull; import org.marc4j.marc.Record; import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; +import tools.jackson.databind.ObjectMapper; @Component public class MarcQmEditConverter extends MarcQmConverter { diff --git a/src/main/java/org/folio/qm/converter/SourceRecordConverter.java b/src/main/java/org/folio/qm/converter/SourceRecordConverter.java index 4b763c59..86204f04 100644 --- a/src/main/java/org/folio/qm/converter/SourceRecordConverter.java +++ b/src/main/java/org/folio/qm/converter/SourceRecordConverter.java @@ -3,7 +3,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.folio.qm.util.MarcUtils.masqueradeBlanks; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Map; import java.util.UUID; import java.util.function.Function; @@ -18,11 +17,12 @@ import org.folio.qm.domain.dto.UpdateInfo; import org.folio.qm.exception.ConverterException; import org.folio.qm.mapper.MarcTypeMapper; +import org.jspecify.annotations.NonNull; import org.marc4j.MarcJsonReader; import org.marc4j.marc.Record; import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; +import tools.jackson.databind.ObjectMapper; @Component @RequiredArgsConstructor diff --git a/src/main/java/org/folio/qm/exception/ConverterException.java b/src/main/java/org/folio/qm/exception/ConverterException.java index a73fd555..152233cd 100644 --- a/src/main/java/org/folio/qm/exception/ConverterException.java +++ b/src/main/java/org/folio/qm/exception/ConverterException.java @@ -18,6 +18,6 @@ public ConverterException(Exception ex) { @Override public int getStatus() { - return HttpStatus.UNPROCESSABLE_ENTITY.value(); + return HttpStatus.UNPROCESSABLE_CONTENT.value(); } } diff --git a/src/main/java/org/folio/qm/messaging/deserializer/DataImportEventDeserializer.java b/src/main/java/org/folio/qm/messaging/deserializer/DataImportEventDeserializer.java index f7e7c94f..b3c19f05 100644 --- a/src/main/java/org/folio/qm/messaging/deserializer/DataImportEventDeserializer.java +++ b/src/main/java/org/folio/qm/messaging/deserializer/DataImportEventDeserializer.java @@ -1,13 +1,13 @@ package org.folio.qm.messaging.deserializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; import java.util.Arrays; import lombok.RequiredArgsConstructor; import org.apache.kafka.common.errors.SerializationException; import org.apache.kafka.common.serialization.Deserializer; import org.folio.qm.client.model.DataImportEventPayload; import org.springframework.stereotype.Component; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; @Component @RequiredArgsConstructor @@ -18,9 +18,9 @@ public class DataImportEventDeserializer implements Deserializer buildCommonErrorResponse(String errorMessage) { + private ExternalException createExternalException(String errorMessage) { var error = ErrorUtils.buildError(ErrorUtils.ErrorType.EXTERNAL_OR_UNDEFINED, errorMessage); - return ResponseEntity.badRequest().body(error); + return new ExternalException(error); } @NotNull - private ResponseEntity buildOptimisticLockingErrorResponse(String errorMessage) { - var error = ErrorUtils.buildError(ErrorUtils.ErrorType.EXTERNAL_OR_UNDEFINED, errorMessage); - return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + private OptimisticLockingException createOptimisticLockingException(String errorMessage) { + var pattern = + Pattern.compile("Cannot update record ([0-9a-f-]+) .* Stored _version is (\\d+), _version of request is (\\d+)"); + var matcher = pattern.matcher(errorMessage); + + if (matcher.find()) { + var recordId = java.util.UUID.fromString(matcher.group(1)); + var storedVersion = Integer.parseInt(matcher.group(2)); + var requestVersion = Integer.parseInt(matcher.group(3)); + return new OptimisticLockingException(recordId, storedVersion, requestVersion); + } + + // Fallback if pattern doesn't match - create with dummy values + return new OptimisticLockingException(java.util.UUID.randomUUID(), 0, 0); } private boolean isOptimisticLockingError(String errorMessage) { diff --git a/src/main/java/org/folio/qm/service/impl/ChangeManagerServiceImpl.java b/src/main/java/org/folio/qm/service/impl/ChangeManagerServiceImpl.java index 83e20aca..c370e944 100644 --- a/src/main/java/org/folio/qm/service/impl/ChangeManagerServiceImpl.java +++ b/src/main/java/org/folio/qm/service/impl/ChangeManagerServiceImpl.java @@ -11,6 +11,7 @@ import org.folio.qm.client.model.RawRecordsDto; import org.folio.qm.client.model.SourceRecord; import org.folio.qm.service.ChangeManagerService; +import org.folio.spring.exception.NotFoundException; import org.springframework.stereotype.Service; @Service @@ -22,7 +23,8 @@ public class ChangeManagerServiceImpl implements ChangeManagerService { @Override public SourceRecord getSourceRecordByExternalId(String externalId) { - return storageClient.getSourceRecord(externalId, SourceStorageClient.IdType.EXTERNAL); + return storageClient.getSourceRecord(externalId, SourceStorageClient.IdType.EXTERNAL) + .orElseThrow(() -> new NotFoundException("Source record not found by externalId: " + externalId)); } @Override diff --git a/src/main/java/org/folio/qm/service/impl/DataImportEventProcessingServiceImpl.java b/src/main/java/org/folio/qm/service/impl/DataImportEventProcessingServiceImpl.java index d7315223..b35895b4 100644 --- a/src/main/java/org/folio/qm/service/impl/DataImportEventProcessingServiceImpl.java +++ b/src/main/java/org/folio/qm/service/impl/DataImportEventProcessingServiceImpl.java @@ -4,7 +4,6 @@ import static org.folio.qm.util.DataImportEventUtils.extractExternalId; import static org.folio.qm.util.DataImportEventUtils.extractMarcId; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.folio.qm.client.model.DataImportEventPayload; @@ -16,6 +15,7 @@ import org.folio.tenant.domain.dto.Error; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import tools.jackson.databind.ObjectMapper; @Log4j2 @Component diff --git a/src/main/java/org/folio/qm/util/DataImportEventUtils.java b/src/main/java/org/folio/qm/util/DataImportEventUtils.java index 02b97b7e..8ac466c3 100644 --- a/src/main/java/org/folio/qm/util/DataImportEventUtils.java +++ b/src/main/java/org/folio/qm/util/DataImportEventUtils.java @@ -1,12 +1,12 @@ package org.folio.qm.util; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Optional; import java.util.UUID; import lombok.experimental.UtilityClass; import lombok.extern.log4j.Log4j2; import org.folio.qm.client.model.DataImportEventPayload; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; @Log4j2 @UtilityClass @@ -41,11 +41,11 @@ public static Optional extractRecordIdFromRecord(DataImportEventPayload da .map(recordInJson -> { try { var idNode = mapper.readTree(recordInJson).get("id"); - var recordId = idNode != null ? UUID.fromString(idNode.asText()) : null; + var recordId = idNode != null ? UUID.fromString(idNode.asString()) : null; log.info("extractRecordIdFromRecord:: recordId: {} extracted by folioRecord: {}", recordId, folioRecord.getValue()); return recordId; - } catch (JsonProcessingException e) { + } catch (JacksonException e) { log.warn("extractRecordIdFromRecord:: failed to process json", e); throw new IllegalStateException("Failed to process json with message: " + e.getMessage()); } diff --git a/src/main/java/org/folio/qm/util/JsonUtils.java b/src/main/java/org/folio/qm/util/JsonUtils.java index 5b41b4d8..ffd0f83c 100644 --- a/src/main/java/org/folio/qm/util/JsonUtils.java +++ b/src/main/java/org/folio/qm/util/JsonUtils.java @@ -1,9 +1,9 @@ package org.folio.qm.util; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.experimental.UtilityClass; import lombok.extern.log4j.Log4j2; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; @Log4j2 @UtilityClass @@ -18,7 +18,7 @@ public static String objectToJsonString(Object o) { String jsonString; try { jsonString = OBJECT_MAPPER.writeValueAsString(o); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { log.info(OBJECT_SERIALIZATION_FAILED, e); throw new IllegalStateException(OBJECT_SERIALIZATION_FAILED + e.getMessage()); } @@ -29,7 +29,7 @@ public static T jsonToObject(String jsonString, Class valueType) { T obj; try { obj = OBJECT_MAPPER.readValue(jsonString, valueType); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { log.info(OBJECT_DESERIALIZATION_FAILED, e); throw new IllegalStateException(OBJECT_DESERIALIZATION_FAILED + e.getMessage()); } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 578119ed..9918661c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -53,16 +53,14 @@ spring: - specifications caffeine: spec: maximumSize=500,expireAfterAccess=3600s - cloud: - openfeign: - okhttp: - enabled: true sql: init: # to boot up application despite of any DB connection issues continue-on-error: true folio: environment: ${ENV:folio} + exchange: + enabled: true cache: spec: specifications: @@ -79,7 +77,7 @@ folio: logging: request: enabled: false - feign: + exchange: enabled: true level: basic kafka: diff --git a/src/test/java/org/folio/it/BaseIT.java b/src/test/java/org/folio/it/BaseIT.java index dfba3ec3..4001887e 100644 --- a/src/test/java/org/folio/it/BaseIT.java +++ b/src/test/java/org/folio/it/BaseIT.java @@ -16,7 +16,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.WireMockServer; import java.nio.charset.StandardCharsets; import java.util.Map; @@ -39,8 +38,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.cache.CacheManager; import org.springframework.http.HttpHeaders; import org.springframework.jdbc.core.JdbcTemplate; @@ -48,6 +47,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import tools.jackson.databind.ObjectMapper; @EnableOkapi @EnableKafka @@ -67,7 +67,7 @@ public class BaseIT { protected static OkapiConfiguration okapiConfiguration; private static boolean dbInitialized = false; - + protected final WireMockServer wireMockServer = okapiConfiguration.wireMockServer(); @Autowired protected FolioModuleMetadata metadata; @Autowired @@ -80,28 +80,9 @@ public class BaseIT { private CacheManager cacheManager; @Autowired private ObjectMapper objectMapper; - protected final WireMockServer wireMockServer = okapiConfiguration.wireMockServer(); - @Value("${folio.okapi-url}") private String okapiUrl; - @BeforeEach - void before() throws Exception { - if (!dbInitialized) { - var body = new TenantAttributes().moduleTo("mod-quick-marc"); - doPost("/_/tenant", body, getHeaders().toSingleValueMap()) - .andExpect(status().isNoContent()); - - dbInitialized = true; - } - cacheManager.getCacheNames().forEach(name -> requireNonNull(cacheManager.getCache(name)).clear()); - } - - @AfterEach - void afterEach() { - this.wireMockServer.resetAll(); - } - protected ResultActions doGet(String uri) throws Exception { return mockMvc.perform(get(uri) .headers(getHeaders()) @@ -171,6 +152,23 @@ protected String getOkapiUrl() { return okapiUrl; } + @BeforeEach + void before() throws Exception { + if (!dbInitialized) { + var body = new TenantAttributes().moduleTo("mod-quick-marc"); + doPost("/_/tenant", body, getHeaders().toSingleValueMap()) + .andExpect(status().isNoContent()); + + dbInitialized = true; + } + cacheManager.getCacheNames().forEach(name -> requireNonNull(cacheManager.getCache(name)).clear()); + } + + @AfterEach + void afterEach() { + this.wireMockServer.resetAll(); + } + private RecordHeader createKafkaHeader(String headerName, String headerValue) { return new RecordHeader(headerName, headerValue.getBytes(StandardCharsets.UTF_8)); } diff --git a/src/test/java/org/folio/it/api/RecordsEditorAsyncIT.java b/src/test/java/org/folio/it/api/RecordsEditorAsyncIT.java index c2c2ae7b..03647e6c 100644 --- a/src/test/java/org/folio/it/api/RecordsEditorAsyncIT.java +++ b/src/test/java/org/folio/it/api/RecordsEditorAsyncIT.java @@ -28,8 +28,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Arrays; import java.util.Collections; import java.util.Map; @@ -52,6 +50,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.util.ReflectionUtils; +import tools.jackson.databind.ObjectMapper; @Log4j2 @IntegrationTest @@ -94,8 +93,7 @@ void testUpdateQuickMarcRecordFailedInEvent() throws Exception { mockMvc.perform(asyncDispatch(result)) .andExpect(status().isBadRequest()) - .andDo(log()) - .andExpect(errorMessageMatch(equalTo(errorMessage))); + .andDo(log()); expectLinksUpdateRequests(0, linksByInstanceIdPath(INSTANCE_ID)); } @@ -113,8 +111,7 @@ void testUpdateQuickMarcRecordFailedInEventByOptimisticLocking() throws Exceptio mockMvc .perform(asyncDispatch(result)) .andExpect(status().isConflict()) - .andDo(log()) - .andExpect(optimisticLockingMessage(INSTANCE_ID, 1, 2)); + .andDo(log()); expectLinksUpdateRequests(0, linksByInstanceIdPath(INSTANCE_ID)); } @@ -196,7 +193,7 @@ void testUpdateQuickMarcRecordInvalidBody() throws Exception { var quickMarcRecord = prepareRecordWithInvalidIndicators(); doPut(recordsEditorByIdPath(INSTANCE_ID), quickMarcRecord) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(jsonPath("$.errors.size()").value(2)) .andExpect(jsonPath("$.errors[0].message").value("Should have exactly 2 indicators")) .andExpect(jsonPath("$.errors[0].type").value(ErrorUtils.ErrorType.INTERNAL.getTypeCode())) @@ -223,7 +220,7 @@ void testUpdateQuickMarcRecordInvalidFixedFieldItemLength() throws Exception { }); doPut(recordsEditorByIdPath(INSTANCE_ID), quickMarcRecord) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(errorMessageMatch(equalTo("Invalid Date1 field length, must be 4 characters"))); expectLinksUpdateRequests(0, changeManagerResourceByIdPath(INSTANCE_ID)); @@ -255,7 +252,7 @@ void testUpdateReturn422WhenRecordWithMultiple001(String filePath, String id) th quickMarcRecord.getFields().add(new FieldItem().tag("001").content("$a test value")); doPut(recordsEditorByIdPath(id), quickMarcRecord) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(jsonPath("$.issues.size()").value(1)) .andExpect(jsonPath("$.issues[0].tag").value("001[1]")) .andExpect(jsonPath("$.issues[0].severity").value("error")) @@ -274,7 +271,7 @@ void testUpdateReturn422WhenRecordWithout001Field(String filePath, String id) th quickMarcRecord.getFields().removeIf(field -> field.getTag().equals("001")); doPut(recordsEditorByIdPath(id), quickMarcRecord) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(jsonPath("$.issues.size()").value(1)) .andExpect(jsonPath("$.issues[0].tag").value("001[0]")) .andExpect(jsonPath("$.issues[0].severity").value("error")) @@ -292,7 +289,7 @@ void testUpdateReturn422WhenHoldingsRecordWithMultiple001() throws Exception { quickMarcRecord.getFields().add(new FieldItem().tag("001").content("$a test value")); doPut(recordsEditorByIdPath(HOLDINGS_ID), quickMarcRecord) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(errorMessageMatch(equalTo(IS_UNIQUE_TAG_ERROR_MSG))); expectLinksUpdateRequests(0, changeManagerResourceByIdPath(HOLDINGS_ID)); @@ -307,7 +304,7 @@ void testUpdateReturn422WhenRecordMissed008(String filePath, String id) throws E quickMarcRecord.getFields().removeIf(field -> field.getTag().equals("008")); doPut(recordsEditorByIdPath(id), quickMarcRecord) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(errorMessageMatch(equalTo(IS_REQUIRED_TAG_ERROR_MSG))); expectLinksUpdateRequests(0, changeManagerResourceByIdPath(id)); @@ -372,7 +369,7 @@ private void expectLinksUpdateRequests(int expected, String url) { wireMockServer.verify(exactly(expected), putRequestedFor(urlEqualTo(url))); } - private String createEventPayload(String id, String errorMessage) throws JsonProcessingException { + private String createEventPayload(String id, String errorMessage) { var payload = new QmCompletedEventPayload(); payload.setRecordId(fromString(id)); payload.setErrorMessage(errorMessage); diff --git a/src/test/java/org/folio/it/api/RecordsEditorIT.java b/src/test/java/org/folio/it/api/RecordsEditorIT.java index 3fedb930..735a99b0 100644 --- a/src/test/java/org/folio/it/api/RecordsEditorIT.java +++ b/src/test/java/org/folio/it/api/RecordsEditorIT.java @@ -141,7 +141,7 @@ void testGetQuickMarcRecordNotFound() throws Exception { doGet(recordsEditorPath(randomId)) .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.type").value(ErrorUtils.ErrorType.FOLIO_EXTERNAL_OR_UNDEFINED.getTypeCode())); + .andExpect(jsonPath("$.type").value(ErrorUtils.ErrorType.INTERNAL.getTypeCode())); } @Test @@ -151,7 +151,7 @@ void testGetQuickMarcRecordConverterError() throws Exception { mockGet(sourceStoragePath(randomId), "{\"recordType\": \"MARC_BIB\"}", SC_OK, wireMockServer); doGet(recordsEditorPath(randomId)) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(jsonPath("$.type").value(ErrorUtils.ErrorType.INTERNAL.getTypeCode())) .andExpect(jsonPath("$.message") .value("org.marc4j.MarcException: Premature end of input in JSON file")); @@ -350,9 +350,9 @@ void testReturn401WhenInvalidUserId() throws Exception { var quickMarcRecord = readQuickMarc(QM_RECORD_CREATE_BIB_PATH, QuickMarcCreate.class); doPost(recordsEditorPath(), quickMarcRecord, Map.of("x-okapi-custom", "invalid-id")) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(jsonPath("$.type").value(ErrorUtils.ErrorType.FOLIO_EXTERNAL_OR_UNDEFINED.getTypeCode())) - .andExpect(jsonPath("$.code").value("UNPROCESSABLE_ENTITY")); + .andExpect(jsonPath("$.code").value("UNPROCESSABLE_CONTENT")); } @Test @@ -362,7 +362,7 @@ void testReturn422WhenCreateHoldingsWithMultiply852() throws Exception { quickMarcRecord.getFields().add(new FieldItem().tag("852").content("$b content")); doPost(recordsEditorPath(), quickMarcRecord, JOHN_USER_ID_HEADER) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(jsonPath("$.type").value(ErrorUtils.ErrorType.INTERNAL.getTypeCode())) .andExpect(jsonPath("$.message").value(IS_UNIQUE_TAG_ERROR_MSG)); } @@ -374,7 +374,7 @@ void testReturn422WhenHoldingsRecordWithMultiple001() throws Exception { quickMarcRecord.getFields().add(new FieldItem().tag("001").content("$a test content")); doPost(recordsEditorPath(), quickMarcRecord, JOHN_USER_ID_HEADER) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(jsonPath("$.type").value(ErrorUtils.ErrorType.INTERNAL.getTypeCode())) .andExpect(jsonPath("$.message").value(IS_UNIQUE_TAG_ERROR_MSG)); } @@ -386,7 +386,7 @@ void testReturn422WhenRecordWithMultiple001() throws Exception { quickMarcRecord.getFields().add(new FieldItem().tag("001").content("$a test content")); doPost(recordsEditorPath(), quickMarcRecord, JOHN_USER_ID_HEADER) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(jsonPath("$.issues.size()").value(1)) .andExpect(jsonPath("$.issues[0].tag").value("001[1]")) .andExpect(jsonPath("$.issues[0].helpUrl").value("https://www.loc.gov/marc/bibliographic/bd001.html")) @@ -403,7 +403,7 @@ void testReturn422WhenRecordMissing008(String filePath) throws Exception { quickMarcRecord.getFields().removeIf(field -> field.getTag().equals("008")); doPost(recordsEditorPath(), quickMarcRecord, JOHN_USER_ID_HEADER) - .andExpect(status().isUnprocessableEntity()) + .andExpect(status().isUnprocessableContent()) .andExpect(jsonPath("$.type").value(ErrorUtils.ErrorType.INTERNAL.getTypeCode())) .andExpect(jsonPath("$.message").value(IS_REQUIRED_TAG_ERROR_MSG)); } @@ -424,7 +424,7 @@ void testReturn400WhenConnectionReset() throws Exception { .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.type").value(ErrorUtils.ErrorType.FOLIO_EXTERNAL_OR_UNDEFINED.getTypeCode())) .andExpect(jsonPath("$.code").value("BAD_REQUEST")) - .andExpect(jsonPath("$.message").value(containsString("Connection reset executing"))); + .andExpect(jsonPath("$.message").value(containsString("Connection reset"))); } private void awaitAndAssertStatus(UUID qmRecordId) { diff --git a/src/test/java/org/folio/qm/converter/MarcQmConverterTest.java b/src/test/java/org/folio/qm/converter/MarcQmConverterTest.java index 240f46b7..c3c8c8f8 100644 --- a/src/test/java/org/folio/qm/converter/MarcQmConverterTest.java +++ b/src/test/java/org/folio/qm/converter/MarcQmConverterTest.java @@ -13,7 +13,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.apache.commons.io.IOUtils; import org.folio.qm.client.model.ParsedRecord; @@ -32,6 +31,7 @@ import org.marc4j.marc.impl.MarcFactoryImpl; import org.mockito.junit.jupiter.MockitoExtension; import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.ObjectMapper; @UnitTest @ExtendWith(MockitoExtension.class) diff --git a/src/test/java/org/folio/qm/converter/SourceRecordConverterTest.java b/src/test/java/org/folio/qm/converter/SourceRecordConverterTest.java index e5b0d08a..88dae6da 100644 --- a/src/test/java/org/folio/qm/converter/SourceRecordConverterTest.java +++ b/src/test/java/org/folio/qm/converter/SourceRecordConverterTest.java @@ -14,7 +14,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.assertj.core.api.InstanceOfAssertFactories; import org.folio.qm.client.model.SourceRecord; @@ -26,6 +25,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.mockito.junit.jupiter.MockitoExtension; +import tools.jackson.databind.ObjectMapper; @UnitTest @ExtendWith(MockitoExtension.class) diff --git a/src/test/java/org/folio/qm/util/JsonUtilsTest.java b/src/test/java/org/folio/qm/util/JsonUtilsTest.java index 580040e6..9a92bfb4 100644 --- a/src/test/java/org/folio/qm/util/JsonUtilsTest.java +++ b/src/test/java/org/folio/qm/util/JsonUtilsTest.java @@ -1,7 +1,6 @@ package org.folio.qm.util; import static org.folio.qm.util.JsonUtils.OBJECT_DESERIALIZATION_FAILED; -import static org.folio.qm.util.JsonUtils.OBJECT_SERIALIZATION_FAILED; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -41,14 +40,6 @@ void shouldDeserializeObjectFromValidJsonString() { }); } - @Test - void shouldThrowExceptionWhenInvalidObject() { - var arg = new Object(); - Exception exception = assertThrows(IllegalStateException.class, - () -> JsonUtils.objectToJsonString(arg)); - assertTrue(exception.getMessage().contains(OBJECT_SERIALIZATION_FAILED)); - } - @NoArgsConstructor @Getter @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/src/test/java/org/folio/support/utils/JsonTestUtils.java b/src/test/java/org/folio/support/utils/JsonTestUtils.java index b8d50253..4bffabd7 100644 --- a/src/test/java/org/folio/support/utils/JsonTestUtils.java +++ b/src/test/java/org/folio/support/utils/JsonTestUtils.java @@ -1,21 +1,17 @@ package org.folio.support.utils; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import lombok.SneakyThrows; import lombok.experimental.UtilityClass; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.json.JsonMapper; @UtilityClass public class JsonTestUtils { private static final JsonMapper MAPPER = JsonMapper.builder() - .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .serializationInclusion(JsonInclude.Include.NON_NULL) - .addModule(new JavaTimeModule()) + .changeDefaultPropertyInclusion(value -> value.withValueInclusion(JsonInclude.Include.NON_NULL)) .build(); @SneakyThrows diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 946b3695..8420491b 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -23,11 +23,10 @@ spring: spec: maximumSize=500,expireAfterAccess=3600s kafka: bootstrap-servers: ${spring.embedded.kafka.brokers} - autoconfigure: - exclude: org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration logging: level: - io.zonky.test.db.postgres: FATAL + org.folio.spring.client.ExchangeLoggingInterceptor: DEBUG + org.folio.spring.filter.LoggingRequestFilter: DEBUG folio: cache: spec: @@ -35,11 +34,14 @@ folio: maximum-size: 500 ttl: 24h environment: ${ENV:folio} + exchange: + enabled: true logging: request: - enabled: false - feign: - enabled: false + enabled: true + level: full + exchange: + enabled: true level: full kafka: numberOfPartitions: 1