Skip to content

Commit

Permalink
MDEXP-741 - Data export should provide UnprocessableEntity response a…
Browse files Browse the repository at this point in the history
…ccording karate scenarios (#459)

* MDEXP-741 - unprocessable entity

* MDEXP-741 - update tests

* MDEXP-741 - update type checking

* MDEXP-741 - update type checking

* MDEXP-741 - update type transformation validation

* MDEXP-741 - fix tests
  • Loading branch information
alekGbuz authored Mar 26, 2024
1 parent e849b3f commit 7b2af2c
Show file tree
Hide file tree
Showing 9 changed files with 475 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,15 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.folio.dataexp.client.UserClient;
import org.folio.dataexp.domain.dto.MappingProfile;
import org.folio.dataexp.domain.dto.MappingProfileCollection;
import org.folio.dataexp.domain.dto.Metadata;
import org.folio.dataexp.domain.dto.UserInfo;
import org.folio.dataexp.domain.entity.MappingProfileEntity;
import org.folio.dataexp.exception.mapping.profile.DefaultMappingProfileException;
import org.folio.dataexp.repository.MappingProfileEntityCqlRepository;
import org.folio.dataexp.repository.MappingProfileEntityRepository;
import org.folio.dataexp.rest.resource.MappingProfilesApi;
import org.folio.spring.FolioExecutionContext;
import org.folio.spring.data.OffsetRequest;
import org.folio.dataexp.service.MappingProfileService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.UUID;

@RestController
Expand All @@ -29,92 +19,35 @@
@RequestMapping("/data-export")
public class MappingProfileController implements MappingProfilesApi {

private final FolioExecutionContext folioExecutionContext;
private final MappingProfileEntityRepository mappingProfileEntityRepository;
private final MappingProfileEntityCqlRepository mappingProfileEntityCqlRepository;
private final UserClient userClient;
private final MappingProfileService mappingProfileService;

@Override
public ResponseEntity<Void> deleteMappingProfileById(UUID mappingProfileId) {
var mappingProfileEntity = mappingProfileEntityRepository.getReferenceById(mappingProfileId);
if (Boolean.TRUE.equals(mappingProfileEntity.getMappingProfile().getDefault()))
throw new DefaultMappingProfileException("Deletion of default mapping profile is forbidden");
mappingProfileEntityRepository.deleteById(mappingProfileId);
mappingProfileService.deleteMappingProfileById(mappingProfileId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}

@Override
public ResponseEntity<MappingProfile> getMappingProfileById(UUID mappingProfileId) {
var mappingProfileEntity = mappingProfileEntityRepository.getReferenceById(mappingProfileId);
var mappingProfileEntity = mappingProfileService.getMappingProfileById(mappingProfileId);
return new ResponseEntity<>(mappingProfileEntity.getMappingProfile(), HttpStatus.OK);
}

@Override
public ResponseEntity<MappingProfileCollection> getMappingProfiles(String query, Integer offset, Integer limit) {
if (StringUtils.isEmpty(query)) query = "(cql.allRecords=1)";
var mappingProfilesPage = mappingProfileEntityCqlRepository.findByCql(query, OffsetRequest.of(offset, limit));
var mappingProfiles = mappingProfilesPage.stream().map(MappingProfileEntity::getMappingProfile).toList();
var mappingProfileCollection = new MappingProfileCollection();
mappingProfileCollection.setMappingProfiles(mappingProfiles);
mappingProfileCollection.setTotalRecords((int) mappingProfilesPage.getTotalElements());
var mappingProfileCollection = mappingProfileService.getMappingProfiles(query, offset, limit);
return new ResponseEntity<>(mappingProfileCollection, HttpStatus.OK);
}

@Override
public ResponseEntity<MappingProfile> postMappingProfile(MappingProfile mappingProfile) {
var userId = folioExecutionContext.getUserId().toString();
var user = userClient.getUserById(userId);
var userInfo = new UserInfo();
userInfo.setFirstName(user.getPersonal().getFirstName());
userInfo.setLastName(user.getPersonal().getLastName());
userInfo.setUserName(user.getUsername());
mappingProfile.setUserInfo(userInfo);

var metaData = new Metadata();
metaData.createdByUserId(userId);
metaData.updatedByUserId(userId);
var current = new Date();
metaData.createdDate(current);
metaData.updatedDate(current);

metaData.createdByUsername(user.getUsername());
metaData.updatedByUsername(user.getUsername());
mappingProfile.setMetadata(metaData);

var saved = mappingProfileEntityRepository.save(MappingProfileEntity.fromMappingProfile(mappingProfile));
return new ResponseEntity<>(saved.getMappingProfile(), HttpStatus.CREATED);
var saved = mappingProfileService.postMappingProfile(mappingProfile);
return new ResponseEntity<>(saved, HttpStatus.CREATED);
}

@Override
public ResponseEntity<Void> putMappingProfile(UUID mappingProfileId, MappingProfile mappingProfile) {
var mappingProfileEntity = mappingProfileEntityRepository.getReferenceById(mappingProfileId);
if (Boolean.TRUE.equals(mappingProfileEntity.getMappingProfile().getDefault())) {
throw new DefaultMappingProfileException("Editing of default mapping profile is forbidden");
}

var userId = folioExecutionContext.getUserId().toString();
var user = userClient.getUserById(userId);

var userInfo = new UserInfo();
userInfo.setFirstName(user.getPersonal().getFirstName());
userInfo.setLastName(user.getPersonal().getLastName());
userInfo.setUserName(user.getUsername());
mappingProfile.setUserInfo(userInfo);

var metadataOfExistingMappingProfile = mappingProfileEntity.getMappingProfile().getMetadata();

var metadata = Metadata.builder()
.createdDate(metadataOfExistingMappingProfile.getCreatedDate())
.updatedDate(new Date())
.createdByUserId(metadataOfExistingMappingProfile.getCreatedByUserId())
.updatedByUserId(userId)
.createdByUsername(metadataOfExistingMappingProfile.getCreatedByUsername())
.updatedByUsername(user.getUsername())
.build();

mappingProfile.setMetadata(metadata);

mappingProfileEntityRepository.save(MappingProfileEntity.fromMappingProfile(mappingProfile));
mappingProfileService.putMappingProfile(mappingProfileId, mappingProfile);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package org.folio.dataexp.exception;

import jakarta.persistence.EntityNotFoundException;
import org.folio.dataexp.domain.dto.Errors;
import org.folio.dataexp.exception.configuration.SliceSizeValidationException;
import org.folio.dataexp.exception.export.DataExportException;
import org.folio.dataexp.exception.file.definition.FileExtensionException;
import org.folio.dataexp.exception.file.definition.FileSizeException;
import org.folio.dataexp.exception.file.definition.UploadFileException;
import org.folio.dataexp.exception.job.profile.DefaultJobProfileException;
import org.folio.dataexp.exception.mapping.profile.DefaultMappingProfileException;
import org.folio.dataexp.exception.mapping.profile.MappingProfileTransformationEmptyException;
import org.folio.dataexp.exception.mapping.profile.MappingProfileTransformationPatternException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
Expand Down Expand Up @@ -36,6 +39,16 @@ public ResponseEntity<String> handleDefaultMappingProfileException(final Default
return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN);
}

@ExceptionHandler(MappingProfileTransformationPatternException.class)
public ResponseEntity<Errors> handleMappingProfileValidationException(final MappingProfileTransformationPatternException e) {
return new ResponseEntity<>(e.getErrors(), HttpStatus.UNPROCESSABLE_ENTITY);
}

@ExceptionHandler(MappingProfileTransformationEmptyException.class)
public ResponseEntity<String> handleMappingProfileTransformationEmptyException(final MappingProfileTransformationEmptyException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.UNPROCESSABLE_ENTITY);
}

@ExceptionHandler(DefaultJobProfileException.class)
public ResponseEntity<String> handleDefaultJobProfileException(final DefaultJobProfileException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.folio.dataexp.exception.mapping.profile;

public class MappingProfileTransformationEmptyException extends RuntimeException {
public MappingProfileTransformationEmptyException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.folio.dataexp.exception.mapping.profile;

import lombok.Getter;
import org.folio.dataexp.domain.dto.Errors;

public class MappingProfileTransformationPatternException extends RuntimeException {
@Getter
private final Errors errors;

public MappingProfileTransformationPatternException(String message, Errors errors) {
super(message);
this.errors = errors;
}
}
154 changes: 154 additions & 0 deletions src/main/java/org/folio/dataexp/service/MappingProfileService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package org.folio.dataexp.service;

import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.folio.dataexp.client.UserClient;
import org.folio.dataexp.domain.dto.Errors;
import org.folio.dataexp.domain.dto.MappingProfile;
import org.folio.dataexp.domain.dto.MappingProfileCollection;
import org.folio.dataexp.domain.dto.Metadata;
import org.folio.dataexp.domain.dto.ParametersInner;
import org.folio.dataexp.domain.dto.RecordTypes;
import org.folio.dataexp.domain.dto.UserInfo;
import org.folio.dataexp.domain.entity.MappingProfileEntity;
import org.folio.dataexp.exception.mapping.profile.DefaultMappingProfileException;
import org.folio.dataexp.exception.mapping.profile.MappingProfileTransformationEmptyException;
import org.folio.dataexp.exception.mapping.profile.MappingProfileTransformationPatternException;
import org.folio.dataexp.repository.MappingProfileEntityCqlRepository;
import org.folio.dataexp.repository.MappingProfileEntityRepository;
import org.folio.spring.FolioExecutionContext;
import org.folio.spring.data.OffsetRequest;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;

@Service
@RequiredArgsConstructor
public class MappingProfileService {

private static final Pattern TRANSFORMATION_PATTERN = Pattern.compile("((\\d{3}([\\s]|[\\d]|[a-zA-Z]){2}(\\$([a-zA-Z]|[\\d]{1,2}))?)|(^$))");
private static final String ERROR_CODE = "javax.validation.constraints.Pattern.message";
private static final String ERROR_VALIDATION_PARAMETER_KEY_PATTERN = "transformations[%s].transformation";
private static final String ERROR_VALIDATION_MESSAGE_PATTERN = "must match \\\"%s\\\"";
private static final String TRANSFORMATION_ITEM_EMPTY_VALUE_MESSAGE = "Transformations for fields with item record type cannot be empty. Please provide a value.";

private final FolioExecutionContext folioExecutionContext;
private final MappingProfileEntityRepository mappingProfileEntityRepository;
private final MappingProfileEntityCqlRepository mappingProfileEntityCqlRepository;
private final UserClient userClient;

public void deleteMappingProfileById(UUID mappingProfileId) {
var mappingProfileEntity = mappingProfileEntityRepository.getReferenceById(mappingProfileId);
if (Boolean.TRUE.equals(mappingProfileEntity.getMappingProfile().getDefault()))
throw new DefaultMappingProfileException("Deletion of default mapping profile is forbidden");
mappingProfileEntityRepository.deleteById(mappingProfileId);
}

public MappingProfileEntity getMappingProfileById(UUID mappingProfileId) {
return mappingProfileEntityRepository.getReferenceById(mappingProfileId);
}

public MappingProfileCollection getMappingProfiles(String query, Integer offset, Integer limit) {
if (StringUtils.isEmpty(query)) query = "(cql.allRecords=1)";
var mappingProfilesPage = mappingProfileEntityCqlRepository.findByCql(query, OffsetRequest.of(offset, limit));
var mappingProfiles = mappingProfilesPage.stream().map(MappingProfileEntity::getMappingProfile).toList();
var mappingProfileCollection = new MappingProfileCollection();
mappingProfileCollection.setMappingProfiles(mappingProfiles);
mappingProfileCollection.setTotalRecords((int) mappingProfilesPage.getTotalElements());
return mappingProfileCollection;
}

public MappingProfile postMappingProfile(MappingProfile mappingProfile) {
var userId = folioExecutionContext.getUserId().toString();
var user = userClient.getUserById(userId);
var userInfo = new UserInfo();
userInfo.setFirstName(user.getPersonal().getFirstName());
userInfo.setLastName(user.getPersonal().getLastName());
userInfo.setUserName(user.getUsername());
mappingProfile.setUserInfo(userInfo);

var metaData = new Metadata();
metaData.createdByUserId(userId);
metaData.updatedByUserId(userId);
var current = new Date();
metaData.createdDate(current);
metaData.updatedDate(current);

metaData.createdByUsername(user.getUsername());
metaData.updatedByUsername(user.getUsername());
mappingProfile.setMetadata(metaData);

validateMappingProfileTransformations(mappingProfile);

var saved = mappingProfileEntityRepository.save(MappingProfileEntity.fromMappingProfile(mappingProfile));
return saved.getMappingProfile();
}

public void putMappingProfile(UUID mappingProfileId, MappingProfile mappingProfile) {
var mappingProfileEntity = mappingProfileEntityRepository.getReferenceById(mappingProfileId);
if (Boolean.TRUE.equals(mappingProfileEntity.getMappingProfile().getDefault())) {
throw new DefaultMappingProfileException("Editing of default mapping profile is forbidden");
}

var userId = folioExecutionContext.getUserId().toString();
var user = userClient.getUserById(userId);

var userInfo = new UserInfo();
userInfo.setFirstName(user.getPersonal().getFirstName());
userInfo.setLastName(user.getPersonal().getLastName());
userInfo.setUserName(user.getUsername());
mappingProfile.setUserInfo(userInfo);

var metadataOfExistingMappingProfile = mappingProfileEntity.getMappingProfile().getMetadata();

var metadata = Metadata.builder()
.createdDate(metadataOfExistingMappingProfile.getCreatedDate())
.updatedDate(new Date())
.createdByUserId(metadataOfExistingMappingProfile.getCreatedByUserId())
.updatedByUserId(userId)
.createdByUsername(metadataOfExistingMappingProfile.getCreatedByUsername())
.updatedByUsername(user.getUsername())
.build();

mappingProfile.setMetadata(metadata);

mappingProfileEntityRepository.save(MappingProfileEntity.fromMappingProfile(mappingProfile));
}

private void validateMappingProfileTransformations(MappingProfile mappingProfile) {
var transformations = mappingProfile.getTransformations();
var parameters = new ArrayList<ParametersInner>();
for (int i = 0; i < transformations.size(); i++) {
var transformation = transformations.get(i);
var matcher = TRANSFORMATION_PATTERN.matcher(transformation.getTransformation());
if (!matcher.matches()) {
var parameter = ParametersInner.builder()
.key(String.format(ERROR_VALIDATION_PARAMETER_KEY_PATTERN, i))
.value(transformation.getTransformation()).build();
parameters.add(parameter);
}
}
if (!parameters.isEmpty()) {
var errors = new Errors();
for (var parameter : parameters) {
var errorItem = new org.folio.dataexp.domain.dto.Error();
errorItem.setCode(ERROR_CODE);
errorItem.type("1");
errorItem.message(String.format(ERROR_VALIDATION_MESSAGE_PATTERN, TRANSFORMATION_PATTERN));
errors.addErrorsItem(errorItem);
errorItem.setParameters(List.of(parameter));
}
errors.setTotalRecords(errors.getErrors().size());
throw new MappingProfileTransformationPatternException("Mapping profile validation exception", errors);
}
for (var transformation : transformations) {
if (StringUtils.isEmpty(transformation.getTransformation()) && transformation.getRecordType() == RecordTypes.ITEM) {
throw new MappingProfileTransformationEmptyException(TRANSFORMATION_ITEM_EMPTY_VALUE_MESSAGE);
}
}
}
}
2 changes: 2 additions & 0 deletions src/main/resources/swagger.api/data-export.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/errors'
text/plain:
example: "Invalid request"
'500':
description: Internal server errors, e.g. due to misconfiguration
content:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
},
"transformation": {
"description": "Mapping expression",
"type": "string",
"pattern": "((\\d{3}([\\s]|[\\d]|[a-zA-Z]){2}(\\$([a-zA-Z]|[\\d]{1,2}))?)|(^$))"
"type": "string"
},
"recordType": {
"description": "Mapping Profile type",
Expand Down
Loading

0 comments on commit 7b2af2c

Please sign in to comment.