Import of CSV-Data and data structure

This commit is contained in:
Matthias Engelien
2024-09-09 21:13:43 +02:00
parent 6985e0ea87
commit 97d86cd11a
18 changed files with 266 additions and 110 deletions

View File

@@ -5,19 +5,15 @@ import java.time.temporal.ChronoUnit;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter; import org.springframework.data.convert.WritingConverter;
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class DataBaseConfiguration extends AbstractJdbcConfiguration { public class DataBaseConfiguration extends AbstractJdbcConfiguration {
@Override @Override
protected List<?> userConverters() { protected List<?> userConverters() {
return Arrays.asList(new DurationToLongConverter(), new LongToDurationConverter()); return Arrays.asList(new DurationToLongConverter(), new LongToDurationConverter());
@@ -28,7 +24,7 @@ public class DataBaseConfiguration extends AbstractJdbcConfiguration {
@Override @Override
public Long convert(Duration duration) { public Long convert(Duration duration) {
return duration.toNanos(); return duration.toSeconds();
} }
} }
@@ -37,7 +33,7 @@ public class DataBaseConfiguration extends AbstractJdbcConfiguration {
@Override @Override
public Duration convert(Long duration) { public Duration convert(Long duration) {
return Duration.of(duration, ChronoUnit.NANOS); return Duration.of(duration, ChronoUnit.SECONDS);
} }
} }

View File

@@ -1,29 +1,22 @@
package de.etecture.ga.config; package de.etecture.ga.config;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import de.etecture.ga.model.Garage;
import de.etecture.ga.service.GarageImportService; import de.etecture.ga.service.GarageImportService;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@Component @Component
@AllArgsConstructor
public class Setup { public class Setup {
@Autowired private final GarageImportService importService;
private GarageImportService importService;
@PostConstruct @PostConstruct
private void setupData() { private void importData() {
List<Garage> garages = importService.importGarageData();
// Daten in DB übertragen
importService.importGarageData();
} }
} }

View File

@@ -4,20 +4,29 @@ import java.time.Duration;
import java.util.Date; import java.util.Date;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors;
@Data @Data
@Accessors(fluent = true, chain = true)
public class Appointment { public class Appointment {
@Id @Id
private Long id; private Long id;
@Column("GARAGE_ID")
private AggregateReference<Garage, Long> garageId;
private String serviceCode; private String serviceCode;
private String serviceName; private String serviceName;
private Date appointmentTime; private Date appointmentTime;
private Duration duration; private Integer slot = 1;
private Duration duration = Duration.ZERO;
} }

View File

@@ -1,48 +1,63 @@
package de.etecture.ga.model; package de.etecture.ga.model;
import java.util.ArrayList; import java.time.Duration;
import java.util.List; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.MappedCollection;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors;
@Data @Data
@Accessors(fluent = true, chain = true)
public class Garage { public class Garage {
@Id @Id
private Long id; private Long id;
private String code;
private String name; private String name;
private List<Appointment> appointments; private Integer maxAppointments = 1;
private Set<GarageServices> garageServices; @MappedCollection(idColumn = "GARAGE_ID")
private Set<Appointment> appointments = new HashSet<>();
@MappedCollection(idColumn = "GARAGE_ID")
private Set<GarageServices> garageServices = new HashSet<>();
public Garage addAppointment(Appointment appointment) { public Garage addAppointment(Appointment appointment) {
if (this.appointments == null) { boolean added = this.appointments.add(appointment);
this.appointments = new ArrayList<>(); if(!added) {
appointment.slot(appointment.slot() + 1);
this.addAppointment(appointment);
} }
this.appointments.add(appointment);
return this; return this;
} }
public void addService(MDService service) { public Garage addService(MDService service) {
garageServices.add(createGarageService(service)); garageServices.add(createGarageService(service, null));
return this;
} }
private GarageServices createGarageService(MDService service) { public void addService(MDService service, Duration duration) {
garageServices.add(createGarageService(service, duration));
}
private GarageServices createGarageService(MDService service, Duration duration) {
Assert.notNull(service, "Service must not be null"); Assert.notNull(service, "Service must not be null");
Assert.notNull(service.getId(), "Service id, must not be null"); Assert.notNull(service.id(), "Service id, must not be null");
GarageServices garageService = new GarageServices(); duration = duration == null ? service.duration() : duration;
garageService.setMdService(service.getId());
return garageService; return new GarageServices().garageId(AggregateReference.to(this.id()))
.serviceId(AggregateReference.to(service.id())).duration(duration);
} }
} }

View File

@@ -1,9 +1,23 @@
package de.etecture.ga.model; package de.etecture.ga.model;
import java.time.Duration;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors;
@Data @Data
@Accessors(fluent = true, chain = true)
public class GarageServices { public class GarageServices {
private Long mdService; @Column("GARAGE_ID")
private AggregateReference<Garage, Long> garageId;
@Column("SERVICE_ID")
private AggregateReference<MDService, Long> serviceId;
private Duration duration;
} }

View File

@@ -5,8 +5,10 @@ import java.time.Duration;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors;
@Data @Data
@Accessors(fluent = true, chain = true)
public class MDService { public class MDService {
@Id @Id
@@ -16,6 +18,6 @@ public class MDService {
private String name; private String name;
private Duration duration; private Duration duration = Duration.ZERO;
} }

View File

@@ -0,0 +1,9 @@
package de.etecture.ga.repository;
import org.springframework.data.repository.CrudRepository;
import de.etecture.ga.model.Appointment;
public interface AppointmentRepository extends CrudRepository<Appointment, Long> {
}

View File

@@ -0,0 +1,12 @@
package de.etecture.ga.repository;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;
import de.etecture.ga.model.Garage;
public interface GarageRepository extends CrudRepository<Garage, Long> {
public Optional<Garage> findByCode(String code);
}

View File

@@ -0,0 +1,20 @@
package de.etecture.ga.repository;
import java.util.Optional;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import de.etecture.ga.model.GarageServices;
import de.etecture.ga.model.MDService;
public interface GarageServiceRepository extends CrudRepository<GarageServices, Long> {
@Query("select s.ID, s.CODE, s.NAME, ifNull(gs.DURATION, s.DURATION) as DURATION "
+ "from GARAGE_SERVICES gs "
+ "join MD_SERVICE s on s.ID = gs.SERVICE_ID "
+ "join GARAGE g on g.ID = gs.GARAGE_ID "
+ "where s.CODE = :serviceCode and g.CODE = :garageCode")
public Optional<MDService> findByServiceCodeAndGarage(String serviceCode, String garageCode);
}

View File

@@ -0,0 +1,12 @@
package de.etecture.ga.repository;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;
import de.etecture.ga.model.MDService;
public interface MDServiceRepository extends CrudRepository<MDService, Long> {
public Optional<MDService> findByCode(String code);
}

View File

@@ -5,7 +5,6 @@ import java.net.URISyntaxException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -13,6 +12,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.MappingIterator; import com.fasterxml.jackson.databind.MappingIterator;
@@ -21,42 +21,54 @@ import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import de.etecture.ga.model.Appointment; import de.etecture.ga.model.Appointment;
import de.etecture.ga.model.Garage; import de.etecture.ga.model.Garage;
import de.etecture.ga.model.MDService;
import de.etecture.ga.repository.AppointmentRepository;
import de.etecture.ga.repository.GarageRepository;
import de.etecture.ga.repository.GarageServiceRepository;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@Service @Service
@AllArgsConstructor
public class GarageImportService { public class GarageImportService {
private static final String IMPORT_FOLDER = "import"; private static final String IMPORT_FOLDER = "import";
private final GarageRepository garageRepository;
public List<Garage> importGarageData() { private final GarageServiceRepository garageServiceRepository;
List<Garage> importData = new ArrayList<>(); private final AppointmentRepository appointmentRepository;
public void importGarageData() {
try (Stream<Path> files = Files try (Stream<Path> files = Files
.list(Paths.get(getClass().getClassLoader().getResource(IMPORT_FOLDER).toURI()))) { .list(Paths.get(getClass().getClassLoader().getResource(IMPORT_FOLDER).toURI()))) {
files.filter(Files::isRegularFile).filter(path -> path.toString().endsWith(".csv")) files.filter(Files::isRegularFile).filter(path -> path.toString().endsWith(".csv"))
.map(this::loadGarageData).<Garage>mapMulti(Optional::ifPresent).forEach(importData::add); .forEach(this::loadGarageData);
} catch (IOException | URISyntaxException e) { } catch (IOException | URISyntaxException e) {
log.error("Can't read file", e); log.error("Can't read file", e);
} }
return importData;
} }
public Optional<Garage> loadGarageData(final Path file) { public Optional<Garage> loadGarageData(final Path file) {
Garage garage = new Garage(); Optional<Garage> garage = garageRepository.findByCode(getGarageNameFromFile(file));
garage.setName(getGarageNameFromFile(file));
List<CSVData> appointments = loadObjectsFromFile(file); if (garage.isPresent()) {
appointments.stream().map(this::fromCSVData).filter(Objects::nonNull) List<CSVData> appointmentData = loadObjectsFromFile(file);
.forEach(garage::addAppointment);
return Optional.of(garage); appointmentData.stream().filter(Objects::nonNull)
.forEach(data -> addAppointmentToGarage(data, garage.get()));
// bestehende Termine speichern
garage.get().appointments().forEach(appointmentRepository::save);
}
return garage;
} }
private String getGarageNameFromFile(final Path fileName) { private String getGarageNameFromFile(final Path fileName) {
@@ -87,16 +99,18 @@ public class GarageImportService {
} }
} }
private Appointment fromCSVData(CSVData data) { private void addAppointmentToGarage(CSVData data, Garage garage) {
if (data == null) Optional<MDService> garageService = garageServiceRepository.findByServiceCodeAndGarage(data.SERVICE,
return null; garage.code());
Appointment appointment = new Appointment(); garageService.map(service -> getAppointmentForService(service, data.APP_DATE, garage.id()))
appointment.setAppointmentTime(data.APP_DATE); .ifPresent(garage::addAppointment);
appointment.setServiceCode(data.SERVICE); }
return appointment; private Appointment getAppointmentForService(MDService service, Date date, Long garageId) {
return new Appointment().appointmentTime(date).serviceCode(service.code()).serviceName(service.name())
.duration(service.duration()).garageId(AggregateReference.to(garageId));
} }
private record CSVData(Date APP_DATE, String SERVICE) { private record CSVData(Date APP_DATE, String SERVICE) {

View File

@@ -0,0 +1,39 @@
package de.etecture.ga.service;
import java.security.InvalidParameterException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import de.etecture.ga.model.MDService;
import de.etecture.ga.repository.MDServiceRepository;
import lombok.AllArgsConstructor;
@Service
@AllArgsConstructor
public class MDServiceService {
private final MDServiceRepository serviceRepository;
public MDService storeMDService(String serviceCode) {
if (StringUtils.isBlank(serviceCode))
throw new InvalidParameterException("serviceCode should not been empty");
MDService service = serviceRepository.findByCode(serviceCode).orElse(new MDService().code(serviceCode));
return serviceRepository.save(service);
}
public MDService storeMDService(MDService serviceToSafe) {
Assert.notNull(serviceToSafe, "Service must not be null");
Assert.notNull(serviceToSafe.code(), "Service code must not be null");
Assert.isTrue(serviceToSafe.duration().isPositive(), "Service duration must must be bigger then 0");
MDService service = serviceRepository.findByCode(serviceToSafe.code()).orElse(serviceToSafe);
return serviceRepository.save(service);
}
}

View File

@@ -0,0 +1,25 @@
-- initial data
INSERT INTO MD_SERVICE (CODE, NAME, DURATION)
VALUES
('MOT', 'Motorinstandsetzung', 14400),
('OIL', 'Ölwechsel', 900),
('WHE', 'Radwechsel', 1800),
('FIX', 'Blechreparatur', 10800),
('INS', 'Hauptuntersuchung', 3600);
INSERT INTO GARAGE (CODE, NAME, MAX_APPOINTMENTS)
VALUES
('autohaus-schmidt', 'Autohaus Schmidt', 2),
('meisterbetrieb-bachstraße', 'Meisterbetrieb Bachstraße', 3);
INSERT INTO GARAGE_SERVICES (GARAGE_ID, SERVICE_ID, DURATION)
VALUES
(1, 1, 14400),
(1, 2, 900),
(1, 3, 1800),
(2, 2, 600),
(2, 4, 10800),
(2, 5, 3600);

View File

@@ -1,41 +1,42 @@
CREATE TABLE GARAGE ( CREATE TABLE GARAGE (
ID BIGINT NOT NULL AUTO_INCREMENT UNIQUE, ID BIGINT NOT NULL AUTO_INCREMENT UNIQUE,
NAME VARCHAR(200) DEFAULT NULL UNIQUE, CODE VARCHAR(100) DEFAULT NULL UNIQUE,
NAME VARCHAR(200) DEFAULT NULL,
MAX_APPOINTMENTS INT NOT NULL DEFAULT 1,
PRIMARY KEY (ID) PRIMARY KEY (ID)
); );
CREATE TABLE APPOINTMENT ( CREATE TABLE APPOINTMENT (
ID BIGINT NOT NULL AUTO_INCREMENT UNIQUE, ID BIGINT NOT NULL AUTO_INCREMENT UNIQUE,
GARAGE BIGINT NOT NULL, GARAGE_ID BIGINT NOT NULL,
SERVICE_CODE VARCHAR(5) DEFAULT NULL, SERVICE_CODE VARCHAR(5) DEFAULT NULL,
SERVICE_NAME VARCHAR(50) DEFAULT NULL, SERVICE_NAME VARCHAR(50) DEFAULT NULL,
APPOINTMENT_TIME DATE DEFAULT NULL, APPOINTMENT_TIME DATE DEFAULT NULL,
SLOT INT NOT NULL DEFAULT 1,
DURATION BIGINT NOT NULL DEFAULT 0, DURATION BIGINT NOT NULL DEFAULT 0,
CONSTRAINT GARAGE_APPOINTMENT_IDX UNIQUE (GARAGE,APPOINTMENT_TIME),
PRIMARY KEY (ID), PRIMARY KEY (ID),
FOREIGN KEY (GARAGE) REFERENCES GARAGE(ID) FOREIGN KEY (GARAGE_ID) REFERENCES GARAGE(ID)
); );
CREATE TABLE MD_SERVICE ( CREATE TABLE MD_SERVICE (
ID BIGINT NOT NULL AUTO_INCREMENT UNIQUE, ID BIGINT NOT NULL AUTO_INCREMENT UNIQUE,
CODE VARCHAR(5) NOT NULL UNIQUE, CODE VARCHAR(5) NOT NULL UNIQUE,
NAME VARCHAR(50) DEFAULT NULL, NAME VARCHAR(50) DEFAULT NULL,
DURATION INT NOT NULL, DURATION BIGINT DEFAULT 0,
PRIMARY KEY (ID) PRIMARY KEY (ID)
); );
CREATE TABLE GARAGE_SERVICES_MAP ( CREATE TABLE GARAGE_SERVICES (
GARAGE BIGINT NOT NULL, GARAGE_ID BIGINT NOT NULL,
MD_SERVICE BIGINT NOT NULL, SERVICE_ID BIGINT NOT NULL,
DURATION BIGINT DEFAULT NULL,
CONSTRAINT GARAGE_SERVICE_IDX UNIQUE (GARAGE,MD_SERVICE), CONSTRAINT GARAGE_SERVICE_IDX UNIQUE (GARAGE_ID, SERVICE_ID),
FOREIGN KEY (GARAGE) REFERENCES GARAGE(ID), FOREIGN KEY (GARAGE_ID) REFERENCES GARAGE(ID),
FOREIGN KEY (MD_SERVICE) REFERENCES MD_SERVICE(ID) FOREIGN KEY (SERVICE_ID) REFERENCES MD_SERVICE(ID)
); );

View File

@@ -1,13 +1,36 @@
package de.etecture.ga; package de.etecture.ga;
import static org.assertj.core.api.Assertions.assertThat;
import java.net.URISyntaxException;
import java.util.Optional;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import de.etecture.ga.model.Garage;
import de.etecture.ga.repository.GarageRepository;
@SpringBootTest @SpringBootTest
class GarageAppointmentManagementApplicationTests { class GarageAppointmentManagementApplicationTests {
@Autowired
private GarageRepository garageRepository;
@Test @Test
void contextLoads() { void contextLoads() {
} }
@Test
void testImportedGarageData() throws URISyntaxException {
Optional<Garage> testGarage = garageRepository.findByCode("test-data");
assertThat(testGarage).isPresent();
assertThat(testGarage.get().name()).isEqualTo("Test Autohaus");
assertThat(testGarage.get().appointments()).hasSize(19);
assertThat(testGarage.get().garageServices()).hasSize(3);
}
} }

View File

@@ -1,40 +0,0 @@
package de.etecture.ga.service;
import static org.assertj.core.api.Assertions.assertThat;
import java.net.URISyntaxException;
import java.util.List;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import de.etecture.ga.model.Appointment;
import de.etecture.ga.model.Garage;
import lombok.extern.slf4j.Slf4j;
@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Slf4j
class GarageImportServiceTest {
@InjectMocks
private GarageImportService service;
@Test
void testImportGarageData() throws URISyntaxException {
List<Garage> garageData = service.importGarageData();
assertThat(garageData).isNotEmpty().hasOnlyElementsOfType(Garage.class).hasSize(1);
Garage garageToTest = garageData.get(0);
assertThat(garageToTest.getName()).isEqualTo("test_data");
assertThat(garageToTest.getAppointments()).hasOnlyElementsOfType(Appointment.class).hasSize(19);
}
}

View File

@@ -0,0 +1,12 @@
-- test data
INSERT INTO GARAGE (CODE, NAME, MAX_APPOINTMENTS)
VALUES
('test-data', 'Test Autohaus', 2);
INSERT INTO GARAGE_SERVICES (GARAGE_ID, SERVICE_ID, DURATION)
VALUES
(select id from GARAGE where CODE = 'test-data', 1, 14400),
(select id from GARAGE where CODE = 'test-data', 2, 900),
(select id from GARAGE where CODE = 'test-data', 3, 1800);