Implemented bulk import of persons

This commit is contained in:
Murat Özkorkmaz
2025-11-04 12:29:32 +01:00
parent 60dc35961a
commit d85406f0c7
19 changed files with 1134 additions and 4 deletions

View File

@@ -0,0 +1,42 @@
package de.iwomm.propify_api.controller;
import de.iwomm.propify_api.service.PersonImportService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@RestController
@RequestMapping("/api/v1/data-import")
public class DataImportController {
private final PersonImportService personImportService;
public DataImportController(PersonImportService personImportService) {
this.personImportService = personImportService;
}
@PostMapping("/persons")
public ResponseEntity<?> importPersons(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("Please upload a file");
}
if (!file.getOriginalFilename().endsWith(".xlsx")) {
return ResponseEntity.badRequest().body("Only .xlsx files are supported");
}
try {
PersonImportService.ImportResult result = personImportService.importPersonsFromExcel(file);
return ResponseEntity.ok(result);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error processing file: " + e.getMessage());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Unexpected error: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,117 @@
package de.iwomm.propify_api.dto;
import com.alibaba.excel.annotation.ExcelProperty;
public class PersonImportDTO {
@ExcelProperty(index = 0, value = "Person - Name")
private String name;
@ExcelProperty(index = 1, value = "Person - Label")
private String labels;
@ExcelProperty(index = 2, value = "Person - Organisation")
private String organization;
@ExcelProperty(index = 3, value = "Person - E-Mail-Adresse - Büro")
private String emailOffice;
@ExcelProperty(index = 4, value = "Person - E-Mail-Adresse - Privat")
private String emailPrivate;
@ExcelProperty(index = 5, value = "Person - E-Mail-Adresse - Sonstiger")
private String emailOther;
@ExcelProperty(index = 6, value = "Person - Telefon - Büro")
private String phoneOffice;
@ExcelProperty(index = 7, value = "Person - Telefon - Privat")
private String phonePrivate;
@ExcelProperty(index = 8, value = "Person - Telefon - Mobil")
private String phoneMobile;
@ExcelProperty(index = 9, value = "Person - Telefon - Sonstiger")
private String phoneOther;
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLabels() {
return labels;
}
public void setLabels(String labels) {
this.labels = labels;
}
public String getOrganization() {
return organization;
}
public void setOrganization(String organization) {
this.organization = organization;
}
public String getEmailOffice() {
return emailOffice;
}
public void setEmailOffice(String emailOffice) {
this.emailOffice = emailOffice;
}
public String getEmailPrivate() {
return emailPrivate;
}
public void setEmailPrivate(String emailPrivate) {
this.emailPrivate = emailPrivate;
}
public String getEmailOther() {
return emailOther;
}
public void setEmailOther(String emailOther) {
this.emailOther = emailOther;
}
public String getPhoneOffice() {
return phoneOffice;
}
public void setPhoneOffice(String phoneOffice) {
this.phoneOffice = phoneOffice;
}
public String getPhonePrivate() {
return phonePrivate;
}
public void setPhonePrivate(String phonePrivate) {
this.phonePrivate = phonePrivate;
}
public String getPhoneMobile() {
return phoneMobile;
}
public void setPhoneMobile(String phoneMobile) {
this.phoneMobile = phoneMobile;
}
public String getPhoneOther() {
return phoneOther;
}
public void setPhoneOther(String phoneOther) {
this.phoneOther = phoneOther;
}
}

View File

@@ -0,0 +1,31 @@
package de.iwomm.propify_api.entity;
import jakarta.persistence.*;
import java.util.UUID;
@Entity
public class Industry {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(nullable = false)
private UUID id;
@Column(nullable = false)
private String name;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,60 @@
package de.iwomm.propify_api.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
public class Organization {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(nullable = false)
private UUID id;
@Column(nullable = false)
private String name;
@ManyToOne()
@JoinColumn(name = "industry_id")
private Industry industry;
@Column(nullable = false)
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
}
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Industry getIndustry() {
return industry;
}
public void setIndustry(Industry industry) {
this.industry = industry;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}

View File

@@ -0,0 +1,31 @@
package de.iwomm.propify_api.entity;
import jakarta.persistence.*;
import java.util.UUID;
@Entity
public class PersonLabel {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(nullable = false)
private UUID id;
@Column(nullable = false)
private String name;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,11 @@
package de.iwomm.propify_api.repository;
import de.iwomm.propify_api.entity.Industry;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface IndustryRepository extends JpaRepository<Industry, UUID> {
Optional<Industry> findByName(String name);
}

View File

@@ -0,0 +1,11 @@
package de.iwomm.propify_api.repository;
import de.iwomm.propify_api.entity.Organization;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface OrganizationRepository extends JpaRepository<Organization, UUID> {
Optional<Organization> findByName(String name);
}

View File

@@ -0,0 +1,11 @@
package de.iwomm.propify_api.repository;
import de.iwomm.propify_api.entity.PersonLabel;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface PersonLabelRepository extends JpaRepository<PersonLabel, UUID> {
Optional<PersonLabel> findByName(String name);
}

View File

@@ -0,0 +1,11 @@
package de.iwomm.propify_api.repository;
import de.iwomm.propify_api.entity.Person;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface PersonRepository extends JpaRepository<Person, UUID> {
Optional<Person> findByKeycloakId(String keycloakId);
}

View File

@@ -0,0 +1,229 @@
package de.iwomm.propify_api.service;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import de.iwomm.propify_api.dto.PersonImportDTO;
import de.iwomm.propify_api.entity.Person;
import de.iwomm.propify_api.entity.PersonLabel;
import de.iwomm.propify_api.repository.OrganizationRepository;
import de.iwomm.propify_api.repository.PersonLabelRepository;
import de.iwomm.propify_api.repository.PersonRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class PersonImportService {
private final PersonRepository personRepository;
private final PersonLabelRepository personLabelRepository;
private final OrganizationRepository organizationRepository;
public PersonImportService(PersonRepository personRepository,
PersonLabelRepository personLabelRepository,
OrganizationRepository organizationRepository) {
this.personRepository = personRepository;
this.personLabelRepository = personLabelRepository;
this.organizationRepository = organizationRepository;
}
@Transactional
public ImportResult importPersonsFromExcel(MultipartFile file) throws IOException {
ImportResult result = new ImportResult();
EasyExcel.read(file.getInputStream(), PersonImportDTO.class, new ReadListener<PersonImportDTO>() {
private int currentRow = 1; // Start at 1 to account for header
@Override
public void invoke(PersonImportDTO data, AnalysisContext context) {
currentRow++;
try {
Person person = convertToPerson(data);
personRepository.save(person);
result.incrementSuccess();
} catch (Exception e) {
result.addError(currentRow, e.getMessage());
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// Called after all data has been analyzed
}
}).sheet().doRead();
return result;
}
private Person convertToPerson(PersonImportDTO dto) {
Person person = new Person();
// Parse name (format: "firstName lastName")
if (dto.getName() != null && !dto.getName().trim().isEmpty()) {
String[] nameParts = dto.getName().trim().split("\\s+", 2);
person.setFirstName(nameParts[0]);
person.setLastName(nameParts.length > 1 ? nameParts[1] : null);
} else {
person.setFirstName(null);
person.setLastName(null);
}
// Parse labels (comma-separated)
if (dto.getLabels() != null && !dto.getLabels().trim().isEmpty()) {
Set<PersonLabel> labels = new HashSet<>();
String[] labelNames = dto.getLabels().split(",");
for (String labelName : labelNames) {
String trimmedName = labelName.trim();
if (!trimmedName.isEmpty()) {
PersonLabel label = personLabelRepository.findByName(trimmedName)
.orElseGet(() -> {
PersonLabel newLabel = new PersonLabel();
newLabel.setName(trimmedName);
return personLabelRepository.save(newLabel);
});
labels.add(label);
}
}
person.setLabels(labels);
}
// Organization (not yet linked in Person entity)
// String organization = dto.getOrganization();
// Email addresses
person.setEmailOffice(trimOrNull(dto.getEmailOffice()));
person.setEmailPrivate(trimOrNull(dto.getEmailPrivate()));
person.setEmailOther(trimOrNull(dto.getEmailOther()));
// Office phone
if (dto.getPhoneOffice() != null && !dto.getPhoneOffice().trim().isEmpty()) {
PhoneNumber parsed = parsePhoneNumber(dto.getPhoneOffice());
person.setPhoneOfficeCountryCode(parsed.countryCode);
person.setPhoneOfficeAreaCode(parsed.areaCode);
person.setPhoneOfficeNumber(parsed.number);
}
// Private phone
if (dto.getPhonePrivate() != null && !dto.getPhonePrivate().trim().isEmpty()) {
PhoneNumber parsed = parsePhoneNumber(dto.getPhonePrivate());
person.setPhonePrivateCountryCode(parsed.countryCode);
person.setPhonePrivateAreaCode(parsed.areaCode);
person.setPhonePrivateNumber(parsed.number);
}
// Mobile phone (prefer phoneMobile, fallback to phoneOther)
String mobile = dto.getPhoneMobile();
String other = dto.getPhoneOther();
if (mobile != null && !mobile.trim().isEmpty()) {
person.setPhoneMobile(mobile.trim());
} else if (other != null && !other.trim().isEmpty()) {
person.setPhoneMobile(other.trim());
}
return person;
}
private String trimOrNull(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
return value.trim();
}
private PhoneNumber parsePhoneNumber(String phone) {
PhoneNumber result = new PhoneNumber();
// Remove all whitespace and special characters except + and digits
String cleaned = phone.replaceAll("[^+\\d]", "");
// Pattern to match: +CountryCode AreaCode Number
// Examples: +493012345678, +4930123456
Pattern pattern = Pattern.compile("^(\\+\\d{1,3})(\\d{2,5})(\\d+)$");
Matcher matcher = pattern.matcher(cleaned);
if (matcher.matches()) {
result.countryCode = matcher.group(1);
result.areaCode = matcher.group(2);
result.number = matcher.group(3);
} else {
// If pattern doesn't match, try simpler parsing
if (cleaned.startsWith("+")) {
// Assume format: +CC followed by rest
int ccEnd = Math.min(4, cleaned.length()); // Max 3 digits + '+'
result.countryCode = cleaned.substring(0, ccEnd);
String rest = cleaned.substring(ccEnd);
if (rest.length() > 4) {
// Split remaining into area code (2-3 digits) and number
result.areaCode = rest.substring(0, Math.min(3, rest.length() - 4));
result.number = rest.substring(Math.min(3, rest.length() - 4));
} else {
result.number = rest;
}
} else {
// No country code, store as-is in number field
result.number = cleaned;
}
}
return result;
}
private static class PhoneNumber {
String countryCode;
String areaCode;
String number;
}
public static class ImportResult {
private int successCount = 0;
private int errorCount = 0;
private List<ImportError> errors = new ArrayList<>();
public void incrementSuccess() {
successCount++;
}
public void addError(int row, String message) {
errorCount++;
errors.add(new ImportError(row, message));
}
public int getSuccessCount() {
return successCount;
}
public int getErrorCount() {
return errorCount;
}
public List<ImportError> getErrors() {
return errors;
}
}
public static class ImportError {
private int row;
private String message;
public ImportError(int row, String message) {
this.row = row;
this.message = message;
}
public int getRow() {
return row;
}
public String getMessage() {
return message;
}
}
}