Skip to content

Commit

Permalink
Merge pull request #122 from JewelleryManagement/feature-87-import-re…
Browse files Browse the repository at this point in the history
…source-by-csv

Feature 87 import resource by csv
  • Loading branch information
LuboKar authored May 19, 2024
2 parents 8584042 + 3d58fd8 commit b856b73
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 47 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@
<artifactId>hypersistence-utils-hibernate-62</artifactId>
<version>3.6.0</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/resources")
Expand Down Expand Up @@ -69,4 +70,11 @@ public ResourceQuantityResponseDto getResourceQuantityById(@PathVariable("id") U
public List<ResourceQuantityResponseDto> getAllResourceQuantities() {
return resourceService.getAllResourceQuantities();
}

@Operation(summary = "Import resources from CSV")
@ResponseStatus(HttpStatus.OK)
@PostMapping("/import")
public List<ResourceResponseDto> importResources(@RequestParam("file") MultipartFile file) {
return resourceService.importResources(file);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package jewellery.inventory.exception.image;

public class MultipartFileContentTypeException extends RuntimeException{
public MultipartFileContentTypeException() {
super("Only PNG or JPG images are allowed.");
}
public class MultipartFileContentTypeException extends RuntimeException {
public MultipartFileContentTypeException() {
super("Only PNG or JPG images are allowed.");
}

public MultipartFileContentTypeException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper(componentModel = "spring")
@Mapper(componentModel = "spring", uses = StringTrimmer.class)
public interface ElementMapper {
ElementMapper INSTANCE = Mappers.getMapper(ElementMapper.class);

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/jewellery/inventory/mapper/MetalMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper(componentModel = "spring")
@Mapper(componentModel = "spring", uses = StringTrimmer.class)
public interface MetalMapper {
MetalMapper INSTANCE = Mappers.getMapper(MetalMapper.class);

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/jewellery/inventory/mapper/PearlMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper(componentModel = "spring")
@Mapper(componentModel = "spring", uses = StringTrimmer.class)
public interface PearlMapper {
PearlMapper INSTANCE = Mappers.getMapper(PearlMapper.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper(componentModel = "spring")
@Mapper(componentModel = "spring", uses = StringTrimmer.class)
public interface PreciousStoneMapper {
PreciousStoneMapper INSTANCE = Mappers.getMapper(PreciousStoneMapper.class);
PreciousStoneMapper INSTANCE = Mappers.getMapper(PreciousStoneMapper.class);

PreciousStoneResponseDto toResourceResponse(PreciousStone entity);
PreciousStoneResponseDto toResourceResponse(PreciousStone entity);

PreciousStone toResourceEntity(PreciousStoneRequestDto dto);
}
PreciousStone toResourceEntity(PreciousStoneRequestDto dto);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper(componentModel = "spring")
@Mapper(componentModel = "spring", uses = StringTrimmer.class)
public interface SemiPreciousStoneMapper {
SemiPreciousStoneMapper INSTANCE = Mappers.getMapper(SemiPreciousStoneMapper.class);
SemiPreciousStoneMapper INSTANCE = Mappers.getMapper(SemiPreciousStoneMapper.class);

SemiPreciousStoneResponseDto toResourceResponse(SemiPreciousStone entity);
SemiPreciousStoneResponseDto toResourceResponse(SemiPreciousStone entity);

SemiPreciousStone toResourceEntity(SemiPreciousStoneRequestDto dto);
SemiPreciousStone toResourceEntity(SemiPreciousStoneRequestDto dto);
}
10 changes: 10 additions & 0 deletions src/main/java/jewellery/inventory/mapper/StringTrimmer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package jewellery.inventory.mapper;

import org.springframework.stereotype.Component;

@Component
public class StringTrimmer {
public String trimString(String value) {
return value.trim();
}
}
1 change: 1 addition & 0 deletions src/main/java/jewellery/inventory/model/EventType.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum EventType {
RESOURCE_TRANSFER,
RESOURCE_REMOVE_QUANTITY,
RESOURCE_ADD_QUANTITY,
RESOURCE_IMPORT,
PRODUCT_CREATE,
PRODUCT_TRANSFER,
PRODUCT_DISASSEMBLY,
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/jewellery/inventory/service/ResourceService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package jewellery.inventory.service;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
Expand All @@ -10,6 +16,8 @@
import jewellery.inventory.dto.request.resource.ResourceRequestDto;
import jewellery.inventory.dto.response.ResourceQuantityResponseDto;
import jewellery.inventory.dto.response.resource.ResourceResponseDto;
import jewellery.inventory.exception.image.MultipartFileContentTypeException;
import jewellery.inventory.exception.image.MultipartFileNotSelectedException;
import jewellery.inventory.exception.not_found.ResourceNotFoundException;
import jewellery.inventory.mapper.ResourceMapper;
import jewellery.inventory.model.EventType;
Expand All @@ -20,6 +28,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -90,6 +99,49 @@ public List<ResourceQuantityResponseDto> getAllResourceQuantities() {
.toList();
}

public List<ResourceResponseDto> importResources(MultipartFile file) {
verifyIsCsv(file);

List<ResourceRequestDto> resourcesDto = getImportedResources(file);

List<ResourceResponseDto> savedResources = new ArrayList<>();
for (ResourceRequestDto resourceRequestDto : resourcesDto) {
ResourceResponseDto savedResource = createResource(resourceRequestDto);
savedResources.add(savedResource);
}

return savedResources;
}

private void verifyIsCsv(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new MultipartFileNotSelectedException();
}

if (!"text/csv".equals(file.getContentType())) {
throw new MultipartFileContentTypeException("Only CSV files are allowed");
}
}

private List<ResourceRequestDto> getImportedResources(MultipartFile file) {
CsvMapper mapper = new CsvMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

CsvSchema schema = CsvSchema.emptySchema().withHeader();

MappingIterator<ResourceRequestDto> resourcesIterator;
List<ResourceRequestDto> resourceDtos;
try {
resourcesIterator =
mapper.readerFor(ResourceRequestDto.class).with(schema).readValues(file.getInputStream());
resourceDtos = resourcesIterator.readAll();
} catch (IOException e) {
throw new RuntimeException(e);
}

return resourceDtos;
}

@Override
public Object fetchEntity(Object... ids) {
ids = Arrays.stream(ids).filter(UUID.class::isInstance).toArray();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
import static jewellery.inventory.model.EventType.RESOURCE_UPDATE;
import static jewellery.inventory.utils.BigDecimalUtil.getBigDecimal;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -28,12 +28,13 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.data.util.Pair;
import org.springframework.data.util.StreamUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.*;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

class ResourceCrudIntegrationTest extends AuthenticatedIntegrationTestBase {
@Autowired private ResourceMapper resourceMapper;
Expand Down Expand Up @@ -128,10 +129,10 @@ void willUpdateResourceToDatabase() throws JsonProcessingException {
assertInputMatchesFetchedFromServer(updatedInputDtos);

Map<String, Object> expectedEventPayload =
getUpdateEventPayload(
createdDtos.get(0),
getMatchingUpdatedDto(createdDtos.get(0).getId(), updatedDtos),
objectMapper);
getUpdateEventPayload(
createdDtos.get(0),
getMatchingUpdatedDto(createdDtos.get(0).getId(), updatedDtos),
objectMapper);

systemEventTestHelper.assertEventWasLogged(RESOURCE_UPDATE, expectedEventPayload);
}
Expand Down Expand Up @@ -182,6 +183,69 @@ void willFailToDeleteResourceFromDatabaseWithWrongId() {
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}

@Test
public void willThrowWhenFileIsEmpty() {
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", getEmptyTestFile().getResource());
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);

ResponseEntity<String> response =
testRestTemplate.postForEntity(getImportUrl(), requestEntity, String.class);

assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}

@Test
public void willThrowWhenFileContentIsWrong() {
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", getTestWrongContentFile().getResource());
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);

ResponseEntity<String> response =
testRestTemplate.postForEntity(getImportUrl(), requestEntity, String.class);

assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}

@Test
public void willImportResourcesSuccessfully() {
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", getTestFile().getResource());
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
ParameterizedTypeReference<List<ResourceResponseDto>> responseType =
new ParameterizedTypeReference<List<ResourceResponseDto>>() {};

ResponseEntity<List<ResourceResponseDto>> response =
testRestTemplate.exchange(getImportUrl(), HttpMethod.POST, requestEntity, responseType);

List<ResourceResponseDto> responseDto = response.getBody();
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(responseDto);
assertEquals(responseDto.get(0).getClazz(), "Element");
assertEquals(responseDto.get(0).getQuantityType(), "28");
assertEquals(responseDto.get(0).getPricePerQuantity(), BigDecimal.valueOf(30));
assertEquals(responseDto.get(0).getNote(), "smth");
}

private MockMultipartFile getEmptyTestFile() {
return new MockMultipartFile("file", "test-file.txt", "text/plain", "".getBytes());
}

private MockMultipartFile getTestWrongContentFile() {
String data = "smth";
return new MockMultipartFile("file", "test-file.txt", "text/plain", data.getBytes());
}

private MockMultipartFile getTestFile() {
String csvData =
"clazz,quantityType,pricePerQuantity,note,description\nElement,28,30,smth,Element description\n";
return new MockMultipartFile("file", "test-file.csv", "text/csv", csvData.getBytes());
}

private String getImportUrl() {
return getBaseResourceUrl() + "/" + "import";
}

@NotNull
private List<ResourceResponseDto> getResourcesWithRequest() throws JsonProcessingException {
String response = this.testRestTemplate.getForObject(getBaseResourceUrl(), String.class);
Expand Down Expand Up @@ -251,10 +315,12 @@ private void assertInputMatchesFetchedFromServer(List<ResourceRequestDto> update

assertThat(mappedDtos).containsExactlyInAnyOrderElementsOf(updatedResources);
}
private ResourceResponseDto getMatchingUpdatedDto(UUID id, List<ResourceResponseDto> updatedDtos) {

private ResourceResponseDto getMatchingUpdatedDto(
UUID id, List<ResourceResponseDto> updatedDtos) {
return updatedDtos.stream()
.filter(resourceResponseDto -> resourceResponseDto.getId().equals(id))
.findFirst()
.orElseThrow(() -> new AssertionFailure("Can't find id: " + id + " in responses"));
.filter(resourceResponseDto -> resourceResponseDto.getId().equals(id))
.findFirst()
.orElseThrow(() -> new AssertionFailure("Can't find id: " + id + " in responses"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,28 @@
import jewellery.inventory.dto.response.resource.ResourceResponseDto;
import jewellery.inventory.exception.MappingException;
import jewellery.inventory.mapper.*;
import jewellery.inventory.mapper.MetalMapper;
import jewellery.inventory.model.resource.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(
classes = {
PearlMapperImpl.class,
PreciousStoneMapperImpl.class,
ElementMapperImpl.class,
MetalMapperImpl.class,
SemiPreciousStoneMapperImpl.class,
StringTrimmer.class,
ResourceMapper.class
})
class ResourceMapperTest {
private ResourceMapper resourceMapper;

@BeforeEach
void setUp() {
PearlMapper pearlMapper = PearlMapper.INSTANCE;
PreciousStoneMapper preciousStoneMapper = PreciousStoneMapper.INSTANCE;
ElementMapper elementMapper = ElementMapper.INSTANCE;
MetalMapper metalMapper = MetalMapper.INSTANCE;
SemiPreciousStoneMapper semiPreciousStoneMapper = SemiPreciousStoneMapper.INSTANCE;
resourceMapper =
new ResourceMapper(pearlMapper, preciousStoneMapper, elementMapper, metalMapper, semiPreciousStoneMapper);
}
@Autowired private ResourceMapper resourceMapper;

@ParameterizedTest
@MethodSource("jewellery.inventory.helper.ResourceTestHelper#provideResourcesAndResponseDtos")
Expand Down
Loading

0 comments on commit b856b73

Please sign in to comment.