Refactor bulk services and enhance database schema.

Replaced `TemplateGenerationService` with `TemplateExportService` and introduced `ExcelMapper` classes for enhanced sheet handling. Updated the database schema to refine node predecessor handling and added new utility methods in repositories. These changes aim to improve bulk operations and data consistency.
This commit is contained in:
Jan 2025-04-15 22:09:47 +02:00
parent 2f58b0b3ba
commit af76f6821b
39 changed files with 1048 additions and 182 deletions

View file

@ -1,81 +0,0 @@
package de.avatic.lcc.dto.generic;
import java.util.Objects;
/**
* Represents a geographical location with latitude and longitude.
* This immutable DTO (Data Transfer Object) is used to transfer location data across system layers.
*/
public class LocationDTO {
/**
* The latitude of the location.
* Positive values indicate north and negative values indicate south.
*/
private final double latitude;
/**
* The longitude of the location.
* Positive values indicate east and negative values indicate west.
*/
private final double longitude;
/**
* Constructs a new {@code LocationDTO} with the specified latitude and longitude.
*
* @param latitude the latitude of the location, where north is positive and south is negative
* @param longitude the longitude of the location, where east is positive and west is negative
*/
public LocationDTO(double latitude, double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
/**
* Default constructor for creating a {@code LocationDTO} at origin (0,0).
* Required for frameworks that use default constructors.
*/
public LocationDTO() {
this(0.0, 0.0);
}
/**
* Gets the latitude of the location.
*
* @return the latitude, where positive values indicate north and negative values indicate south
*/
public double getLatitude() {
return latitude;
}
/**
* Gets the longitude of the location.
*
* @return the longitude, where positive values indicate east and negative values indicate west
*/
public double getLongitude() {
return longitude;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LocationDTO that = (LocationDTO) o;
return Double.compare(that.latitude, latitude) == 0 &&
Double.compare(that.longitude, longitude) == 0;
}
@Override
public int hashCode() {
return Objects.hash(latitude, longitude);
}
@Override
public String toString() {
return "LocationDTO{" +
"latitude=" + latitude +
", longitude=" + longitude +
'}';
}
}

View file

@ -83,10 +83,17 @@
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>org.dhatim</groupId>
<artifactId>fastexcel</artifactId>
<version>0.18.4</version>
</dependency>
</dependencies>
<dependencyManagement>

View file

@ -6,7 +6,7 @@ import de.avatic.lcc.dto.bulk.BulkProcessingType;
import de.avatic.lcc.dto.bulk.BulkStatus;
import de.avatic.lcc.service.bulk.BulkExportService;
import de.avatic.lcc.service.bulk.BulkFileProcessingService;
import de.avatic.lcc.service.bulk.TemplateGenerationService;
import de.avatic.lcc.service.bulk.TemplateExportService;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@ -24,12 +24,12 @@ import org.springframework.web.multipart.MultipartFile;
public class BulkOperationController {
private final BulkFileProcessingService bulkProcessingService;
private final TemplateGenerationService templateGenerationService;
private final TemplateExportService templateExportService;
private final BulkExportService bulkExportService;
public BulkOperationController(BulkFileProcessingService bulkProcessingService, TemplateGenerationService templateGenerationService, BulkExportService bulkExportService) {
public BulkOperationController(BulkFileProcessingService bulkProcessingService, TemplateExportService templateExportService, BulkExportService bulkExportService) {
this.bulkProcessingService = bulkProcessingService;
this.templateGenerationService = templateGenerationService;
this.templateExportService = templateExportService;
this.bulkExportService = bulkExportService;
}
@ -75,7 +75,7 @@ public class BulkOperationController {
.ok()
.headers(headers)
.contentType(MediaType.parseMediaType("application/vnd.ms-excel"))
.body(new InputStreamResource(templateGenerationService.generateTemplate(BulkFileType.valueOf(type.name().toUpperCase()))));
.body(new InputStreamResource(templateExportService.generateTemplate(BulkFileType.valueOf(type.name().toUpperCase()))));
}
/**

View file

@ -19,7 +19,7 @@ public class NodeDetailDTO {
private LocationDTO location;
@JsonProperty("is_deprecated")
private Boolean isDeprecated;
private Map<Integer, NodeDTO> predecessors;
private List<Map<Integer, NodeDTO>> predecessors;
@JsonProperty("outbound_countries")
private List<CountryDTO> outboundCountries;
@ -80,11 +80,11 @@ public class NodeDetailDTO {
isDeprecated = deprecated;
}
public Map<Integer, NodeDTO> getPredecessors() {
public List<Map<Integer, NodeDTO>> getPredecessors() {
return predecessors;
}
public void setPredecessors(Map<Integer, NodeDTO> predecessors) {
public void setPredecessors(List<Map<Integer, NodeDTO>> predecessors) {
this.predecessors = predecessors;
}

View file

@ -0,0 +1,20 @@
package de.avatic.lcc.model.bulk;
public enum BulkFileTypes {
CONTAINER_RATE("Container Rate"), COUNTRY_MATRIX("Country Matrix Rate"), MATERIAL("Material"), PACKAGING("Packaging"), NODE("Nodes");
private final String sheetName;
BulkFileTypes(String sheetName) {
this.sheetName = sheetName;
}
public String getSheetName() {
return sheetName;
}
}

View file

@ -0,0 +1,16 @@
package de.avatic.lcc.model.bulk;
public enum ContainerRateHeader implements HeaderProvider {
FROM_NODE("Origin"), TO_NODE("Destination"), CONTAINER_RATE_TYPE("Transport mode"), RATE_FEU("Rate 40 ft GP [EUR]"),
RATE_TEU("Rate 20 ft GP [EUR]"), RATE_HC("Rate 40 ft HC [EUR]"), LEAD_TIME("Lead time [d]");
private final String header;
ContainerRateHeader(String header) {
this.header = header;
}
public String getHeader() {
return header;
}
}

View file

@ -0,0 +1,5 @@
package de.avatic.lcc.model.bulk;
public interface HeaderProvider {
String getHeader();
}

View file

@ -0,0 +1,16 @@
package de.avatic.lcc.model.bulk;
public enum HiddenCountryHeader implements HeaderProvider{
ISO_CODE("Iso Code"), NAME("Name");
private final String header;
HiddenCountryHeader(String header) {
this.header = header;
}
@Override
public String getHeader() {
return header;
}
}

View file

@ -0,0 +1,15 @@
package de.avatic.lcc.model.bulk;
public enum HiddenNodeHeader implements HeaderProvider {
MAPPING_ID("Mapping Id"), NAME("Name");
private final String header;
HiddenNodeHeader(String header) {
this.header = header;
}
public String getHeader() {
return header;
}
}

View file

@ -0,0 +1,19 @@
package de.avatic.lcc.model.bulk;
public enum HiddenTableType {
COUNTRY_HIDDEN_TABLE("ISOCodes"), NODE_HIDDEN_TABLE("Nodes"), ORIGIN_HIDDEN_TABLE("Origins");
private final String sheetName;
HiddenTableType(String sheetName) {
this.sheetName = sheetName;
}
public String getSheetName() {
return sheetName;
}
public String getReferenceName() {
return "ref_"+sheetName.toLowerCase();
}
}

View file

@ -0,0 +1,18 @@
package de.avatic.lcc.model.bulk;
public enum MaterialHeader implements HeaderProvider {
PART_NUMBER("Part number"),
DESCRIPTION("Material description"),
HS_CODE("HS code");
private final String header;
MaterialHeader(String header) {
this.header = header;
}
public String getHeader() {
return header;
}
}

View file

@ -0,0 +1,17 @@
package de.avatic.lcc.model.bulk;
public enum MatrixRateHeader implements HeaderProvider {
FROM_COUNTRY("Country of origin (ISO 3166-1)"),
TO_COUNTRY("Country of destination (ISO 3166-1)"),
RATE("Cost per km (incl. floater) [EUR]");
private final String header;
MatrixRateHeader(String header) {
this.header = header;
}
public String getHeader() {
return header;
}
}

View file

@ -0,0 +1,19 @@
package de.avatic.lcc.model.bulk;
public enum NodeHeader implements HeaderProvider {
MAPPING_ID("Mapping ID"), NAME("Name"), ADDRESS("Address"),
COUNTRY("Country (ISO 3166-1)"), GEO_LATITUDE("Latitude"), GEO_LONGITUDE("Longitude"),
IS_ORIGIN("Origin"), IS_INTERMEDIATE("Intermediate"), IS_DESTINATION("Destination"),
OUTBOUND_COUNTRIES("Outbound countries (ISO 3166-1)"), PREDECESSOR_NODES("Predecessor Nodes (Mapping ID)"),
IS_PREDECESSOR_MANDATORY("Predecessors mandatory");
private final String header;
NodeHeader(String header) {
this.header = header;
}
public String getHeader() {
return header;
}
}

View file

@ -0,0 +1,32 @@
package de.avatic.lcc.model.bulk;
public enum PackagingHeader implements HeaderProvider {
PART_NUMBER("Part number"),
SUPPLIER("Supplier (Mapping ID)"),
SHU_LENGTH("SHU length"),
SHU_WIDTH("SHU width"),
SHU_HEIGHT("SHU height"),
SHU_DIMENSION_UNIT("SHU Dimension unit"),
SHU_WEIGHT("SHU gross weight"),
SHU_WEIGHT_UNIT("SHU gross weight unit"),
SHU_UNIT_COUNT("Units/SHU [pieces]"),
HU_LENGTH("HU length"),
HU_WIDTH("HU width"),
HU_HEIGHT("HU height"),
HU_DIMENSION_UNIT("HU Dimension unit"),
HU_WEIGHT("HU gross weight"),
HU_WEIGHT_UNIT("HU gross weight unit"),
HU_UNIT_COUNT("Units/HU [pieces]"),
STACKABLE("Stackable");
private final String header;
PackagingHeader(String header) {
this.header = header;
}
public String getHeader() {
return header;
}
}

View file

@ -27,7 +27,7 @@ public class Node {
@NotNull
private Boolean predecessorRequired;
private Boolean isSink;
private Boolean isDestination;
private Boolean isSource;
@ -49,7 +49,7 @@ public class Node {
private Integer countryId;
private Map<Integer, Integer> nodePredecessors;
private List<Map<Integer, Integer>> nodePredecessors;
private Collection<Integer> outboundCountries;
@ -93,12 +93,12 @@ public class Node {
this.predecessorRequired = predecessorRequired;
}
public Boolean getSink() {
return isSink;
public Boolean getDestination() {
return isDestination;
}
public void setSink(Boolean sink) {
isSink = sink;
public void setDestination(Boolean destination) {
isDestination = destination;
}
public Boolean getSource() {
@ -157,11 +157,11 @@ public class Node {
this.countryId = countryId;
}
public Map<Integer, Integer> getNodePredecessors() {
public List<Map<Integer, Integer>> getNodePredecessors() {
return nodePredecessors;
}
public void setNodePredecessors(Map<Integer, Integer> nodePredecessors) {
public void setNodePredecessors(List<Map<Integer, Integer>> nodePredecessors) {
this.nodePredecessors = nodePredecessors;
}

View file

@ -15,6 +15,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@ -28,6 +29,11 @@ public class MaterialRepository {
this.jdbcTemplate = jdbcTemplate;
}
public List<Material> listAllMaterials() {
String query = "SELECT * FROM material ORDER BY normalized_part_number";
return jdbcTemplate.query(query, new MaterialMapper());
}
private static class MaterialMapper implements RowMapper<Material> {

View file

@ -28,7 +28,7 @@ public class NodeRepository {
String query = """
SELECT node.id AS id, node.name AS name, node.address as address, node.is_source as is_source,
node.is_sink as is_sink, node.is_intermediate as is_intermediate, node.country_id as country_id, node.predecessor_required as predecessor_required
node.is_destination as is_destination, node.is_intermediate as is_intermediate, node.country_id as country_id, node.predecessor_required as predecessor_required
FROM node
WHERE node.id = ?""";
@ -37,22 +37,29 @@ public class NodeRepository {
return Optional.ofNullable(node);
}
private Map<Integer, Integer> getPredecessorsOf(Integer id) {
String query = """
SELECT node_predecessor.predecessor_node_id, node_predecessor.sequence_number
FROM node_predecessor
WHERE node_predecessor.node_id = ? ORDER BY node_predecessor.sequence_number""";
private List<Map<Integer, Integer>> getPredecessorsOf(Integer id) {
return jdbcTemplate.query(query, rs -> {
Map<Integer, Integer> predecessors = new HashMap<>();
String queryChains = """
SELECT chain.id AS id FROM node_predecessor_chain AS chain WHERE chain.node_id = ? ORDER BY chain.id
""";
while(rs.next()) {
predecessors.put(rs.getInt("sequence_number"), rs.getInt("predecessor_node_id"));
}
return jdbcTemplate.query(queryChains, (chainRs, rowNum) -> {
String query = """
SELECT entry.node_id AS predecessor , entry.sequence_number as sequence_number
FROM node_predecessor_entry AS entry
WHERE entry.node_predecessor_chain_id = ? ORDER BY entry.sequence_number""";
return predecessors;
return jdbcTemplate.query(query, rs -> {
Map<Integer, Integer> predecessors = new HashMap<>();
while (rs.next()) {
predecessors.put(rs.getInt("sequence_number"), rs.getInt("predecessor_node_id"));
}
return predecessors;
}, id);
}, id);
}
};
private Collection<Integer> getOutboundCountriesOf(Integer id) {
String query = """
@ -70,12 +77,11 @@ public class NodeRepository {
List<Node> entities = null;
Integer totalCount = 0;
if(filter == null) {
if (filter == null) {
entities = jdbcTemplate.query(query, new NodeMapper(), pagination.getLimit(), pagination.getOffset());
totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class);
}
else {
entities= jdbcTemplate.query(query, new NodeMapper(), "%" + filter + "%", "%" + filter + "%", "%" + filter + "%", pagination.getLimit(), pagination.getOffset());
} else {
entities = jdbcTemplate.query(query, new NodeMapper(), "%" + filter + "%", "%" + filter + "%", "%" + filter + "%", pagination.getLimit(), pagination.getOffset());
totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class, "%" + filter + "%", "%" + filter + "%", "%" + filter + "%");
}
@ -131,24 +137,33 @@ public class NodeRepository {
}
public List<Node> searchNode(String filter, int limit, NodeType nodeType, boolean excludeDeprecated) {
StringBuilder queryBuilder = new StringBuilder().append("SELECT * FROM node WHERE (name LIKE ? OR address LIKE ?)");
StringBuilder queryBuilder = new StringBuilder().append("SELECT * FROM node WHERE (name LIKE ? OR address LIKE ?)");
if(nodeType != null) {
if (nodeType != null) {
queryBuilder.append(" AND node_type = ?");
}
if(excludeDeprecated) {
if (excludeDeprecated) {
queryBuilder.append(" AND is_deprecated = false");
}
queryBuilder.append(" LIMIT ?");
if(nodeType != null)
{
return jdbcTemplate.query(queryBuilder.toString(),new NodeMapper(), "%" + filter + "%", "%" + filter + "%", nodeType.name(), limit);
if (nodeType != null) {
return jdbcTemplate.query(queryBuilder.toString(), new NodeMapper(), "%" + filter + "%", "%" + filter + "%", nodeType.name(), limit);
}
return jdbcTemplate.query(queryBuilder.toString(),new NodeMapper(), "%" + filter + "%", "%" + filter + "%", limit);
return jdbcTemplate.query(queryBuilder.toString(), new NodeMapper(), "%" + filter + "%", "%" + filter + "%", limit);
}
public List<Node> listAllNodes(boolean onlySources) {
StringBuilder queryBuilder = new StringBuilder("SELECT * FROM node");
if (onlySources) {
queryBuilder.append(" WHERE is_source = true");
}
queryBuilder.append(" ORDER BY id");
return jdbcTemplate.query(queryBuilder.toString(), new NodeMapper());
}
private class NodeMapper implements RowMapper<Node> {
@ -161,7 +176,7 @@ public class NodeRepository {
data.setName(rs.getString("name"));
data.setAddress(rs.getString("address"));
data.setCountryId(rs.getInt("country_id"));
data.setSink(rs.getBoolean("is_sink"));
data.setDestination(rs.getBoolean("is_destination"));
data.setSource(rs.getBoolean("is_source"));
data.setIntermediate(rs.getBoolean("is_intermediate"));
data.setPredecessorRequired(rs.getBoolean("predecessor_required"));

View file

@ -12,6 +12,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
@Repository
@ -114,6 +115,10 @@ public class CountryRepository {
return queryBuilder.toString();
}
public List<Country> listAllCountries() {
String query = "SELECT * FROM country ORDER BY iso_code";
return jdbcTemplate.query(query, new CountryMapper());
}
private static class CountryMapper implements RowMapper<Country> {

View file

@ -23,6 +23,11 @@ import java.util.Optional;
public class PackagingRepository {
public List<Packaging> listAllPackaging() {
String query = "SELECT * FROM packaging ORDER BY id";
return jdbcTemplate.query(query, new PackagingMapper());
}
private static class PackagingMapper implements RowMapper<Packaging> {
@Override

View file

@ -10,6 +10,7 @@ import org.springframework.stereotype.Repository;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
@Repository
public class ContainerRateRepository {
@ -33,6 +34,12 @@ public class ContainerRateRepository {
return jdbcTemplate.queryForObject(query, new ContainerRateMapper(), id);
}
public List<ContainerRate> listAllRatesByPeriodId(Integer periodId) {
String query = "SELECT * FROM container_rate WHERE validity_period_id = ?";
return jdbcTemplate.query(query, new ContainerRateMapper());
}
private static class ContainerRateMapper implements RowMapper<ContainerRate> {
@Override
public ContainerRate mapRow(ResultSet rs, int rowNum) throws SQLException {

View file

@ -1,6 +1,5 @@
package de.avatic.lcc.repositories.rates;
import de.avatic.lcc.dto.configuration.matrixrates.MatrixRateDTO;
import de.avatic.lcc.model.rates.MatrixRate;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
@ -11,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* Repository for managing {@link MatrixRate} entities, including retrieval and mapping of MatrixRate rows
@ -71,6 +71,11 @@ public class MatrixRateRepository {
return jdbcTemplate.queryForObject(query, new MatrixRateMapper(), id);
}
public List<MatrixRate> listAllRatesByPeriodId(Integer periodId) {
String query = "SELECT * FROM country_matrix_rate WHERE validity_period_id = ?";
return jdbcTemplate.query(query, new MatrixRateMapper());
}
/**
* Maps rows of a {@link ResultSet} to {@link MatrixRate} objects as required by
* the {@link JdbcTemplate}.

View file

@ -1,12 +1,48 @@
package de.avatic.lcc.service.bulk;
import de.avatic.lcc.dto.bulk.BulkFileType;
import de.avatic.lcc.model.bulk.BulkFileTypes;
import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider;
import de.avatic.lcc.service.bulk.mapper.ContainerRateExcelMapper;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.core.io.InputStreamSource;
import org.springframework.stereotype.Service;
@Service
public class BulkExportService {
private final HeaderCellStyleProvider headerCellStyleProvider;
private final ContainerRateExcelMapper containerRateExcelMapper;
public BulkExportService(HeaderCellStyleProvider headerCellStyleProvider, ContainerRateExcelMapper containerRateExcelMapper) {
this.headerCellStyleProvider = headerCellStyleProvider;
this.containerRateExcelMapper = containerRateExcelMapper;
}
public InputStreamSource generateExport(BulkFileType bulkFileType) {
Workbook workbook = new XSSFWorkbook();
Sheet worksheet = workbook.createSheet(BulkFileTypes.valueOf(bulkFileType.name()).getSheetName());
CellStyle style = headerCellStyleProvider.createHeaderCellStyle(workbook);
// Create headers based on the bulk file type
switch (bulkFileType) {
case CONTAINER_RATE:
containerRateExcelMapper.fillSheet();
break;
case COUNTRY_MATRIX:
break;
case MATERIAL:
break;
case PACKAGING:
break;
case NODE:
break;
}
return null;
}

View file

@ -0,0 +1,110 @@
package de.avatic.lcc.service.bulk;
import de.avatic.lcc.dto.bulk.BulkFileType;
import de.avatic.lcc.model.bulk.*;
import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import de.avatic.lcc.service.bulk.mapper.*;
import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@Service
public class TemplateExportService {
private final HeaderGenerator headerGenerator;
private final HeaderCellStyleProvider headerCellStyleProvider;
private final HiddenNodeExcelMapper hiddenNodeExcelMapper;
private final HiddenCountryExcelMapper hiddenCountryExcelMapper;
private final String sheetPassword;
private final ContainerRateExcelMapper containerRateExcelMapper;
private final MatrixRateExcelMapper matrixRateExcelMapper;
private final MaterialExcelMapper materialExcelMapper;
private final PackagingExcelMapper packagingExcelMapper;
private final NodeExcelMapper nodeExcelMapper;
public TemplateExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, HeaderGenerator headerGenerator, HeaderCellStyleProvider headerCellStyleProvider, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper) {
this.headerGenerator = headerGenerator;
this.headerCellStyleProvider = headerCellStyleProvider;
this.hiddenNodeExcelMapper = hiddenNodeExcelMapper;
this.hiddenCountryExcelMapper = hiddenCountryExcelMapper;
this.sheetPassword = sheetPassword;
this.containerRateExcelMapper = containerRateExcelMapper;
this.matrixRateExcelMapper = matrixRateExcelMapper;
this.materialExcelMapper = materialExcelMapper;
this.packagingExcelMapper = packagingExcelMapper;
this.nodeExcelMapper = nodeExcelMapper;
}
public InputStreamSource generateTemplate(BulkFileType bulkFileType) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet(BulkFileTypes.valueOf(bulkFileType.name()).getSheetName());
CellStyle style = headerCellStyleProvider.createHeaderCellStyle(workbook);
Sheet hiddenSheet = null;
// Create headers and constraints based on the bulk file type
switch (bulkFileType) {
case CONTAINER_RATE:
headerGenerator.generateHeader(sheet, ContainerRateHeader.class, style);
containerRateExcelMapper.createConstraints(workbook, sheet);
break;
case COUNTRY_MATRIX:
headerGenerator.generateHeader(sheet, MatrixRateHeader.class, style);
matrixRateExcelMapper.createConstraints(workbook, sheet);
break;
case MATERIAL:
headerGenerator.generateHeader(sheet, MaterialHeader.class, style);
materialExcelMapper.createConstraints(sheet);
break;
case PACKAGING:
headerGenerator.generateHeader(sheet, PackagingHeader.class, style);
packagingExcelMapper.createConstraints(workbook, sheet);
break;
case NODE:
headerGenerator.generateHeader(sheet, NodeHeader.class, style);
nodeExcelMapper.createConstraints(workbook, sheet);
break;
default:
throw new IllegalArgumentException("Unsupported bulk file type: " + bulkFileType);
}
switch (bulkFileType) {
case CONTAINER_RATE:
case PACKAGING:
var hiddenNodeSheet = workbook.createSheet(HiddenTableType.NODE_HIDDEN_TABLE.getSheetName());
hiddenNodeExcelMapper.fillSheet(hiddenNodeSheet, style, BulkFileType.PACKAGING.equals(bulkFileType));
hiddenNodeSheet.protectSheet(sheetPassword);
workbook.createName().setNameName(HiddenTableType.NODE_HIDDEN_TABLE.getSheetName()+"_ref");
break;
case COUNTRY_MATRIX:
case NODE:
var hiddenCountrySheet = workbook.createSheet(HiddenTableType.COUNTRY_HIDDEN_TABLE.getSheetName());
hiddenCountryExcelMapper.fillSheet(hiddenCountrySheet, style);
hiddenCountrySheet.protectSheet(sheetPassword);
workbook.setSheetVisibility(workbook.getSheetIndex(hiddenCountrySheet), SheetVisibility.VERY_HIDDEN);
break;
default:
break;
}
// Return the Excel file as an InputStreamSource
return new ByteArrayResource(outputStream.toByteArray());
} catch (IOException e) {
throw new RuntimeException("Failed to generate template", e);
}
}
}

View file

@ -1,12 +0,0 @@
package de.avatic.lcc.service.bulk;
import de.avatic.lcc.dto.bulk.BulkFileType;
import org.springframework.core.io.InputStreamSource;
import org.springframework.stereotype.Service;
@Service
public class TemplateGenerationService {
public InputStreamSource generateTemplate(BulkFileType bulkFileType) {
}
}

View file

@ -0,0 +1,89 @@
package de.avatic.lcc.service.bulk.helper;
import de.avatic.lcc.model.bulk.HiddenTableType;
import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
import org.apache.poi.ss.usermodel.Name;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.springframework.stereotype.Service;
import java.util.EnumSet;
@Service
public class ConstraintGenerator {
public void createBooleanConstraint(Sheet sheet, Integer columnIdx) {
createConstraint(sheet, columnIdx, new String[]{"true", "false"});
}
public <T extends Enum<T>> void createEnumConstraint(Sheet sheet, Integer columnIdx, Class<T> values) {
createConstraint(sheet, columnIdx, EnumSet.allOf(values).stream().map(Enum::name).toArray(String[]::new));
}
private void createConstraint(Sheet sheet, Integer columnIdx, String[] values) {
var helper = sheet.getDataValidationHelper();
var constraint = helper.createExplicitListConstraint(values);
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx));
validation.createErrorBox("Invalid Value", values.length > 8 ? "Please check dropdown for allowed values." : String.format("Allowed values are: %s.", String.join(", ", values)));
validation.setShowErrorBox(true);
sheet.addValidationData(validation);
}
public void createFormulaListConstraint(Sheet sheet, Integer columnIdx, String namedReference) {
var helper = sheet.getDataValidationHelper();
var constraint = helper.createFormulaListConstraint(namedReference);
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx));
validation.createErrorBox("Invalid Value", "Please check dropdown for allowed values.");
validation.setShowErrorBox(true);
sheet.addValidationData(validation);
}
public void createLengthConstraint(Sheet sheet, Integer columnIdx, Integer min, Integer max) {
var helper = sheet.getDataValidationHelper();
var constraint = helper.createTextLengthConstraint(DataValidationConstraint.OperatorType.BETWEEN, String.valueOf(min), String.valueOf(max));
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx));
validation.createErrorBox("Invalid Value", String.format("Allowed length is between %d and %d characters.", min, max));
validation.setShowErrorBox(true);
sheet.addValidationData(validation);
}
public void createDecimalConstraint(Sheet sheet, Integer columnIdx, Number min, Number max) {
var helper = sheet.getDataValidationHelper();
var constraint = helper.createDecimalConstraint(DataValidationConstraint.OperatorType.BETWEEN, String.valueOf(min), String.valueOf(max));
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx));
validation.createErrorBox("Invalid Value", String.format("Allowed value is between %f and %f.", min.floatValue(), max.floatValue()));
validation.setShowErrorBox(true);
sheet.addValidationData(validation);
}
public void createIntegerConstraint(Sheet sheet, Integer columnIdx, Number min, Number max) {
var helper = sheet.getDataValidationHelper();
var constraint = helper.createIntegerConstraint(DataValidationConstraint.OperatorType.BETWEEN, String.valueOf(min), String.valueOf(max));
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx));
validation.createErrorBox("Invalid Value", String.format("Allowed value is between %f and %f.", min.floatValue(), max.floatValue()));
validation.setShowErrorBox(true);
sheet.addValidationData(validation);
}
public Name createReference(Workbook workbook, Integer columnIdx, HiddenTableType hiddenTableType ) {
Name namedRange = workbook.createName();
namedRange.setNameName(hiddenTableType.getReferenceName());
String reference = hiddenTableType.getSheetName()+ "!$"+toExcelLetter(columnIdx)+"$1:$"+toExcelLetter(columnIdx)+"$" + SpreadsheetVersion.EXCEL2007.getMaxRows();
namedRange.setRefersToFormula(reference);
return namedRange;
}
private String toExcelLetter(int columnIdx) {
int digit = columnIdx % 26;
int quotient = columnIdx / 26;
return (quotient==0 ? "" : toExcelLetter(quotient)) + (char) ('A' + digit);
}
}

View file

@ -0,0 +1,24 @@
package de.avatic.lcc.service.bulk.helper;
import org.apache.poi.ss.usermodel.*;
import org.springframework.stereotype.Service;
@Service
public class HeaderCellStyleProvider {
public CellStyle createHeaderCellStyle(Workbook workbook) {
CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerStyle.setFont(headerFont);
headerStyle.setFillForegroundColor(IndexedColors.LIGHT_CORNFLOWER_BLUE.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
headerStyle.setBorderBottom(BorderStyle.THIN);
headerStyle.setBorderTop(BorderStyle.THIN);
headerStyle.setBorderLeft(BorderStyle.THIN);
headerStyle.setBorderRight(BorderStyle.THIN);
headerStyle.setAlignment(HorizontalAlignment.CENTER);
return headerStyle;
}
}

View file

@ -0,0 +1,40 @@
package de.avatic.lcc.service.bulk.helper;
import de.avatic.lcc.model.bulk.HeaderProvider;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.stereotype.Service;
import java.util.EnumSet;
@Service
public class HeaderGenerator {
public <T extends Enum<T> & HeaderProvider> void generateHeader(Sheet worksheet, Class<T> headers, CellStyle style) {
Row row = worksheet.createRow(0);
for (T header : EnumSet.allOf(headers)) {
Cell cell = row.createCell(header.ordinal());
cell.setCellValue(header.getHeader());
cell.setCellStyle(style);
worksheet.autoSizeColumn(header.ordinal());
}
}
public void generateHeader(Sheet worksheet, String[] headers, CellStyle style) {
Row row = worksheet.createRow(0);
int idx = 0;
for (String header : headers) {
Cell cell = row.createCell(idx);
cell.setCellValue(header);
cell.setCellStyle(style);
worksheet.autoSizeColumn(idx++);
}
}
}

View file

@ -0,0 +1,63 @@
package de.avatic.lcc.service.bulk.mapper;
import de.avatic.lcc.model.bulk.ContainerRateHeader;
import de.avatic.lcc.model.bulk.HiddenNodeHeader;
import de.avatic.lcc.model.bulk.HiddenTableType;
import de.avatic.lcc.model.rates.ContainerRate;
import de.avatic.lcc.model.rates.ContainerRateType;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.rates.ContainerRateRepository;
import de.avatic.lcc.service.bulk.helper.ConstraintGenerator;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.stereotype.Service;
@Service
public class ContainerRateExcelMapper {
private final HeaderGenerator headerGenerator;
private final ContainerRateRepository containerRateRepository;
private final NodeRepository nodeRepository;
private final ConstraintGenerator constraintGenerator;
public ContainerRateExcelMapper(HeaderGenerator headerGenerator, ContainerRateRepository containerRateRepository, NodeRepository nodeRepository, ConstraintGenerator constraintGenerator) {
this.headerGenerator = headerGenerator;
this.containerRateRepository = containerRateRepository;
this.nodeRepository = nodeRepository;
this.constraintGenerator = constraintGenerator;
}
public void fillSheet(Sheet sheet, CellStyle headerStyle, Integer periodId) {
headerGenerator.generateHeader(sheet, ContainerRateHeader.class, headerStyle);
containerRateRepository.listAllRatesByPeriodId(periodId).forEach(rate -> mapToRow(rate, sheet.createRow(sheet.getLastRowNum()+1)));
}
private void mapToRow(ContainerRate rate, Row row) {
row.createCell(ContainerRateHeader.FROM_NODE.ordinal()).setCellValue(nodeRepository.getById(rate.getFromNodeId()).orElseThrow().getExternalMappingId());
row.createCell(ContainerRateHeader.TO_NODE.ordinal()).setCellValue(nodeRepository.getById(rate.getToNodeId()).orElseThrow().getExternalMappingId());
row.createCell(ContainerRateHeader.CONTAINER_RATE_TYPE.ordinal()).setCellValue(rate.getType().name());
row.createCell(ContainerRateHeader.RATE_FEU.ordinal()).setCellValue(rate.getRateFeu().doubleValue());
row.createCell(ContainerRateHeader.RATE_TEU.ordinal()).setCellValue(rate.getRateTeu().doubleValue());
row.createCell(ContainerRateHeader.RATE_HC.ordinal()).setCellValue(rate.getRateHc().doubleValue());
row.createCell(ContainerRateHeader.LEAD_TIME.ordinal()).setCellValue(rate.getLeadTime());
}
public void createConstraints(Workbook workbook, Sheet sheet) {
var namedRange = constraintGenerator.createReference(workbook, HiddenNodeHeader.MAPPING_ID.ordinal(),HiddenTableType.NODE_HIDDEN_TABLE);
constraintGenerator.createFormulaListConstraint(sheet, ContainerRateHeader.FROM_NODE.ordinal(), namedRange.getNameName());
constraintGenerator.createFormulaListConstraint(sheet, ContainerRateHeader.TO_NODE.ordinal(), namedRange.getNameName());
constraintGenerator.createEnumConstraint(sheet, ContainerRateHeader.CONTAINER_RATE_TYPE.ordinal(), ContainerRateType.class);
constraintGenerator.createDecimalConstraint(sheet, ContainerRateHeader.RATE_FEU.ordinal(), 0.0, 1000000.0);
constraintGenerator.createDecimalConstraint(sheet, ContainerRateHeader.RATE_TEU.ordinal(),0.0, 1000000.0);
constraintGenerator.createDecimalConstraint(sheet, ContainerRateHeader.RATE_HC.ordinal(),0.0, 1000000.0);
constraintGenerator.createIntegerConstraint(sheet, ContainerRateHeader.LEAD_TIME.ordinal(), 0, 365);
}
}

View file

@ -0,0 +1,34 @@
package de.avatic.lcc.service.bulk.mapper;
import de.avatic.lcc.model.bulk.HiddenCountryHeader;
import de.avatic.lcc.model.country.Country;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.stereotype.Service;
@Service
public class HiddenCountryExcelMapper {
private final HeaderGenerator headerGenerator;
private final CountryRepository countryRepository;
public HiddenCountryExcelMapper(HeaderGenerator headerGenerator, CountryRepository countryRepository) {
this.headerGenerator = headerGenerator;
this.countryRepository = countryRepository;
}
public void fillSheet(Sheet sheet, CellStyle headerStyle) {
headerGenerator.generateHeader(sheet, HiddenCountryHeader.class, headerStyle);
countryRepository.listAllCountries().forEach(c -> mapToRow(c, sheet.createRow(sheet.getLastRowNum() + 1)));
}
private void mapToRow(Country entity, Row row) {
row.createCell(HiddenCountryHeader.ISO_CODE.ordinal()).setCellValue(entity.getIsoCode().getCode());
row.createCell(HiddenCountryHeader.NAME.ordinal()).setCellValue(entity.getIsoCode().getFullName());
}
}

View file

@ -0,0 +1,33 @@
package de.avatic.lcc.service.bulk.mapper;
import de.avatic.lcc.model.bulk.HiddenNodeHeader;
import de.avatic.lcc.model.nodes.Node;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.stereotype.Service;
@Service
public class HiddenNodeExcelMapper {
private final HeaderGenerator headerGenerator;
private final NodeRepository nodeRepository;
public HiddenNodeExcelMapper(HeaderGenerator headerGenerator, NodeRepository nodeRepository) {
this.headerGenerator = headerGenerator;
this.nodeRepository = nodeRepository;
}
public void fillSheet(Sheet sheet, CellStyle headerStyle, boolean originOnly){
headerGenerator.generateHeader(sheet, HiddenNodeHeader.class, headerStyle);
nodeRepository.listAllNodes(originOnly).forEach(node -> mapToRow(node, sheet.createRow(sheet.getLastRowNum()+1)));
}
private void mapToRow(Node node, Row row) {
row.createCell(HiddenNodeHeader.MAPPING_ID.ordinal()).setCellValue(node.getExternalMappingId());
row.createCell(HiddenNodeHeader.NAME.ordinal()).setCellValue(node.getName());
}
}

View file

@ -0,0 +1,46 @@
package de.avatic.lcc.service.bulk.mapper;
import de.avatic.lcc.model.bulk.HiddenCountryHeader;
import de.avatic.lcc.model.bulk.HiddenTableType;
import de.avatic.lcc.model.bulk.MaterialHeader;
import de.avatic.lcc.model.bulk.MatrixRateHeader;
import de.avatic.lcc.model.materials.Material;
import de.avatic.lcc.repositories.MaterialRepository;
import de.avatic.lcc.service.bulk.helper.ConstraintGenerator;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.stereotype.Service;
@Service
public class MaterialExcelMapper {
private final HeaderGenerator headerGenerator;
private final MaterialRepository materialRepository;
private final ConstraintGenerator constraintGenerator;
public MaterialExcelMapper(HeaderGenerator headerGenerator, MaterialRepository materialRepository, ConstraintGenerator constraintGenerator) {
this.headerGenerator = headerGenerator;
this.materialRepository = materialRepository;
this.constraintGenerator = constraintGenerator;
}
public void fillSheet(Sheet sheet, CellStyle headerStyle) {
headerGenerator.generateHeader(sheet, MaterialHeader.class, headerStyle);
materialRepository.listAllMaterials().forEach(material -> mapToRow(material, sheet.createRow(sheet.getLastRowNum()+1)));
}
private void mapToRow(Material material, Row row) {
row.createCell(MaterialHeader.PART_NUMBER.ordinal()).setCellValue(material.getPartNumber());
row.createCell(MaterialHeader.DESCRIPTION.ordinal()).setCellValue(material.getName());
row.createCell(MaterialHeader.HS_CODE.ordinal()).setCellValue(material.getHsCode());
}
public void createConstraints(Sheet sheet) {
constraintGenerator.createLengthConstraint(sheet, MaterialHeader.PART_NUMBER.ordinal(), 0, 12);
constraintGenerator.createLengthConstraint(sheet, MaterialHeader.HS_CODE.ordinal(), 0, 8);
constraintGenerator.createLengthConstraint(sheet, MaterialHeader.DESCRIPTION.ordinal(), 1, 500);
}
}

View file

@ -0,0 +1,50 @@
package de.avatic.lcc.service.bulk.mapper;
import de.avatic.lcc.model.bulk.*;
import de.avatic.lcc.model.rates.ContainerRateType;
import de.avatic.lcc.model.rates.MatrixRate;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.repositories.rates.MatrixRateRepository;
import de.avatic.lcc.service.bulk.helper.ConstraintGenerator;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.stereotype.Service;
@Service
public class MatrixRateExcelMapper {
private final HeaderGenerator headerGenerator;
private final MatrixRateRepository matrixRateRepository;
private final CountryRepository countryRepository;
private final ConstraintGenerator constraintGenerator;
public MatrixRateExcelMapper(HeaderGenerator headerGenerator, MatrixRateRepository matrixRateRepository, CountryRepository countryRepository, ConstraintGenerator constraintGenerator) {
this.headerGenerator = headerGenerator;
this.matrixRateRepository = matrixRateRepository;
this.countryRepository = countryRepository;
this.constraintGenerator = constraintGenerator;
}
public void fillSheet(Sheet sheet, CellStyle headerStyle, Integer periodId) {
headerGenerator.generateHeader(sheet, MatrixRateHeader.class, headerStyle);
matrixRateRepository.listAllRatesByPeriodId(periodId).forEach(rate -> mapToRow(rate, sheet.createRow(sheet.getLastRowNum()+1)));
}
private void mapToRow(MatrixRate rate, Row row) {
row.createCell(MatrixRateHeader.FROM_COUNTRY.ordinal()).setCellValue(countryRepository.getById(rate.getFromCountry()).orElseThrow().getIsoCode().getCode());
row.createCell(MatrixRateHeader.TO_COUNTRY.ordinal()).setCellValue(countryRepository.getById(rate.getToCountry()).orElseThrow().getIsoCode().getCode());
row.createCell(MatrixRateHeader.RATE.ordinal()).setCellValue(rate.getRate().doubleValue());
}
public void createConstraints(Workbook workbook, Sheet sheet) {
var namedRange = constraintGenerator.createReference(workbook, HiddenCountryHeader.ISO_CODE.ordinal(), HiddenTableType.COUNTRY_HIDDEN_TABLE);
constraintGenerator.createFormulaListConstraint(sheet, MatrixRateHeader.FROM_COUNTRY.ordinal(), namedRange.getNameName());
constraintGenerator.createFormulaListConstraint(sheet, MatrixRateHeader.TO_COUNTRY.ordinal(), namedRange.getNameName());
constraintGenerator.createDecimalConstraint(sheet, MatrixRateHeader.RATE.ordinal(), 0.0, 1000000.0);
}
}

View file

@ -0,0 +1,88 @@
package de.avatic.lcc.service.bulk.mapper;
import de.avatic.lcc.model.bulk.HiddenCountryHeader;
import de.avatic.lcc.model.bulk.HiddenTableType;
import de.avatic.lcc.model.bulk.MatrixRateHeader;
import de.avatic.lcc.model.bulk.NodeHeader;
import de.avatic.lcc.model.country.Country;
import de.avatic.lcc.model.country.IsoCode;
import de.avatic.lcc.model.nodes.Node;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.service.bulk.helper.ConstraintGenerator;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class NodeExcelMapper {
private final HeaderGenerator headerGenerator;
private final NodeRepository nodeRepository;
private final CountryRepository countryRepository;
private final ConstraintGenerator constraintGenerator;
public NodeExcelMapper(HeaderGenerator headerGenerator, NodeRepository nodeRepository, CountryRepository countryRepository, ConstraintGenerator constraintGenerator) {
this.headerGenerator = headerGenerator;
this.nodeRepository = nodeRepository;
this.countryRepository = countryRepository;
this.constraintGenerator = constraintGenerator;
}
public void fillSheet(Sheet sheet, CellStyle headerStyle)
{
headerGenerator.generateHeader(sheet, NodeHeader.class, headerStyle);
nodeRepository.listAllNodes(false).forEach(node -> mapToRow(node, sheet.createRow(sheet.getLastRowNum()+1)));
}
private void mapToRow(Node node, Row row) {
row.createCell(NodeHeader.MAPPING_ID.ordinal()).setCellValue(node.getExternalMappingId());
row.createCell(NodeHeader.NAME.ordinal()).setCellValue(node.getName());
row.createCell(NodeHeader.ADDRESS.ordinal()).setCellValue(node.getAddress());
row.createCell(NodeHeader.COUNTRY.ordinal()).setCellValue(countryRepository.getById(node.getCountryId()).orElseThrow().getIsoCode().getCode());
row.createCell(NodeHeader.GEO_LATITUDE.ordinal()).setCellValue(node.getGeoLat().doubleValue());
row.createCell(NodeHeader.GEO_LONGITUDE.ordinal()).setCellValue(node.getGeoLng().doubleValue());
row.createCell(NodeHeader.IS_ORIGIN.ordinal()).setCellValue(node.getSource());
row.createCell(NodeHeader.IS_INTERMEDIATE.ordinal()).setCellValue(node.getIntermediate());
row.createCell(NodeHeader.IS_DESTINATION.ordinal()).setCellValue(node.getDestination());
row.createCell(NodeHeader.OUTBOUND_COUNTRIES.ordinal()).setCellValue(mapOutboundCountries(node.getOutboundCountries()));
row.createCell(NodeHeader.PREDECESSOR_NODES.ordinal()).setCellValue(mapChains(node.getNodePredecessors()));
row.createCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).setCellValue(node.getPredecessorRequired());
}
private String mapChains(List<Map<Integer, Integer>> chains) {
return chains.stream().map(this::mapChain).collect(Collectors.joining(", "));
}
private String mapChain(Map<Integer, Integer> chain) {
var seq = new ArrayList<>(chain.keySet());
Collections.sort(seq);
return seq.stream().map(s -> nodeRepository.getById(chain.get(s)).orElseThrow().getExternalMappingId()).collect(Collectors.joining(", "));
}
private String mapOutboundCountries(Collection<Integer> outboundCountryIds) {
return outboundCountryIds.stream().map(countryRepository::getById).filter(Optional::isPresent).map(Optional::get).map(Country::getIsoCode).map(IsoCode::getCode).collect(Collectors.joining(", "));
}
public void createConstraints(Workbook workbook, Sheet sheet) {
var namedRange = constraintGenerator.createReference(workbook, HiddenCountryHeader.ISO_CODE.ordinal(), HiddenTableType.COUNTRY_HIDDEN_TABLE);
constraintGenerator.createFormulaListConstraint(sheet, NodeHeader.COUNTRY.ordinal(), namedRange.getNameName());
constraintGenerator.createDecimalConstraint(sheet, NodeHeader.GEO_LATITUDE.ordinal(), -90.0, 90.0);
constraintGenerator.createDecimalConstraint(sheet, NodeHeader.GEO_LONGITUDE.ordinal(), -180.0, 180.0);
constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_ORIGIN.ordinal());
constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_INTERMEDIATE.ordinal());
constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_DESTINATION.ordinal());
constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal());
constraintGenerator.createLengthConstraint(sheet, NodeHeader.ADDRESS.ordinal(), 1, 500);
constraintGenerator.createLengthConstraint(sheet, NodeHeader.NAME.ordinal(), 1, 255);
}
}

View file

@ -0,0 +1,106 @@
package de.avatic.lcc.service.bulk.mapper;
import de.avatic.lcc.model.bulk.*;
import de.avatic.lcc.model.packaging.Packaging;
import de.avatic.lcc.model.packaging.PackagingDimension;
import de.avatic.lcc.model.properties.PropertyType;
import de.avatic.lcc.model.utils.DimensionUnit;
import de.avatic.lcc.repositories.MaterialRepository;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.packaging.PackagingDimensionRepository;
import de.avatic.lcc.repositories.packaging.PackagingPropertiesRepository;
import de.avatic.lcc.repositories.packaging.PackagingRepository;
import de.avatic.lcc.service.bulk.helper.ConstraintGenerator;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import org.apache.poi.ss.usermodel.*;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.Optional;
import java.util.function.Function;
@Service
public class PackagingExcelMapper {
private final HeaderGenerator headerGenerator;
private final PackagingRepository packagingRepository;
private final MaterialRepository materialRepository;
private final NodeRepository nodeRepository;
private final PackagingDimensionRepository packagingDimensionRepository;
private final PackagingPropertiesRepository packagingPropertiesRepository;
private final ConstraintGenerator constraintGenerator;
public PackagingExcelMapper(HeaderGenerator headerGenerator, PackagingRepository packagingRepository, MaterialRepository materialRepository, NodeRepository nodeRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository, ConstraintGenerator constraintGenerator) {
this.headerGenerator = headerGenerator;
this.packagingRepository = packagingRepository;
this.materialRepository = materialRepository;
this.nodeRepository = nodeRepository;
this.packagingDimensionRepository = packagingDimensionRepository;
this.packagingPropertiesRepository = packagingPropertiesRepository;
this.constraintGenerator = constraintGenerator;
}
public void fillSheet(Sheet sheet, CellStyle headerStyle) {
var headers = new ArrayList<>(EnumSet.allOf(PackagingHeader.class).stream().map(PackagingHeader::getHeader).toList());
headers.addAll(packagingPropertiesRepository.listTypes().stream().map(PropertyType::getExternalMappingId).toList());
headerGenerator.generateHeader(sheet, headers.toArray(String[]::new), headerStyle);
packagingRepository.listAllPackaging().forEach(p -> mapToRow(p,headers, sheet.createRow(sheet.getLastRowNum()+1)));
}
private void mapToRow(Packaging packaging, ArrayList<String> headers, Row row) {
Optional<PackagingDimension> shu = packagingDimensionRepository.getById(packaging.getShuId());
Optional<PackagingDimension> hu = packagingDimensionRepository.getById(packaging.getShuId());
row.createCell(PackagingHeader.PART_NUMBER.ordinal()).setCellValue(materialRepository.getById(packaging.getMaterialId()).orElseThrow().getPartNumber());
row.createCell(PackagingHeader.SUPPLIER.ordinal()).setCellValue(nodeRepository.getById(packaging.getSupplierId()).orElseThrow().getExternalMappingId());
mapToCell(row.createCell(PackagingHeader.SHU_HEIGHT.ordinal()), shu, PackagingDimension::getHeight);
mapToCell(row.createCell(PackagingHeader.SHU_WIDTH.ordinal()), shu, PackagingDimension::getWidth);
mapToCell(row.createCell(PackagingHeader.SHU_LENGTH.ordinal()), shu, PackagingDimension::getLength);
mapToCell(row.createCell(PackagingHeader.SHU_WEIGHT.ordinal()), shu, PackagingDimension::getWeight);
mapToCell(row.createCell(PackagingHeader.SHU_UNIT_COUNT.ordinal()), shu, PackagingDimension::getContentUnitCount);
mapToCell(row.createCell(PackagingHeader.SHU_DIMENSION_UNIT.ordinal()), shu, PackagingDimension::getDimensionUnit);
mapToCell(row.createCell(PackagingHeader.SHU_WEIGHT_UNIT.ordinal()), shu, PackagingDimension::getWeightUnit);
mapToCell(row.createCell(PackagingHeader.HU_HEIGHT.ordinal()), hu, PackagingDimension::getHeight);
mapToCell(row.createCell(PackagingHeader.HU_WIDTH.ordinal()), hu, PackagingDimension::getWidth);
mapToCell(row.createCell(PackagingHeader.HU_LENGTH.ordinal()), hu, PackagingDimension::getLength);
mapToCell(row.createCell(PackagingHeader.HU_WEIGHT.ordinal()), hu, PackagingDimension::getWeight);
mapToCell(row.createCell(PackagingHeader.HU_UNIT_COUNT.ordinal()), hu, PackagingDimension::getContentUnitCount);
mapToCell(row.createCell(PackagingHeader.HU_DIMENSION_UNIT.ordinal()), hu, PackagingDimension::getDimensionUnit);
mapToCell(row.createCell(PackagingHeader.HU_WEIGHT_UNIT.ordinal()), hu, PackagingDimension::getWeightUnit);
var properties = packagingPropertiesRepository.getByPackagingId(packaging.getId());
properties.forEach(p -> row.createCell(headers.indexOf(p.getType().getExternalMappingId())).setCellValue(p.getValue()));
}
private <T, R> void mapToCell(Cell cell, Optional<T> dimension, Function<T,R> resolver){
if(dimension.isPresent()) {
if(resolver.apply(dimension.get()) instanceof Integer)
cell.setCellValue((Integer) resolver.apply(dimension.get()));
if(resolver.apply(dimension.get()) instanceof String)
cell.setCellValue((String) resolver.apply(dimension.get()));
if(resolver.apply(dimension.get()) instanceof DimensionUnit)
cell.setCellValue(((DimensionUnit) resolver.apply(dimension.get())).name());
}
else cell.setBlank();
}
public void createConstraints(Workbook workbook, Sheet sheet) {
var namedRange = constraintGenerator.createReference(workbook, HiddenNodeHeader.MAPPING_ID.ordinal(), HiddenTableType.ORIGIN_HIDDEN_TABLE);
constraintGenerator.createFormulaListConstraint(sheet, PackagingHeader.SUPPLIER.ordinal(), namedRange.getNameName());
constraintGenerator.createLengthConstraint(sheet, PackagingHeader.PART_NUMBER.ordinal(), 0,12);
//TODO: check hu dimensions...
//todo check propeties?
}
}

View file

@ -42,11 +42,11 @@ public class NodeService {
}
public SearchQueryResult<NodeDetailDTO> listNodesView(String filter, int page, int limit) {
return SearchQueryResult.map(nodeRepository.listNodes(filter, true, new SearchQueryPagination(page, limit)), nodeDetailTransformer::toNodeViewDTO);
return SearchQueryResult.map(nodeRepository.listNodes(filter, true, new SearchQueryPagination(page, limit)), nodeDetailTransformer::toNodeDetailDTO);
}
public NodeDetailDTO getNode(Integer id) {
return nodeDetailTransformer.toNodeViewDTO(nodeRepository.getById(id).orElseThrow(() -> new NodeNotFoundException(id)));
return nodeDetailTransformer.toNodeDetailDTO(nodeRepository.getById(id).orElseThrow(() -> new NodeNotFoundException(id)));
}
public Integer deleteNode(Integer id) {
@ -82,7 +82,7 @@ public class NodeService {
node.setAddress(dto.getAddress());
node.setGeoLng(BigDecimal.valueOf(dto.getLocation().getLongitude()));
node.setGeoLat(BigDecimal.valueOf(dto.getLocation().getLatitude()));
node.setSink(false);
node.setDestination(false);
node.setSource(true);
node.setIntermediate(false);
node.setDeprecated(false);

View file

@ -25,7 +25,7 @@ public class NodeTransformer {
NodeDTO dto = new NodeDTO();
ArrayList<NodeType> types = new ArrayList<>();
if (entity.getSink()) types.add(NodeType.DESTINATION);
if (entity.getDestination()) types.add(NodeType.DESTINATION);
if (entity.getSource()) types.add(NodeType.SOURCE);
if (entity.getIntermediate()) types.add(NodeType.INTERMEDIATE);

View file

@ -1,11 +1,11 @@
package de.avatic.lcc.service.transformer.nodes;
import de.avatic.lcc.dto.configuration.nodes.view.NodeDetailDTO;
import de.avatic.lcc.dto.generic.NodeDTO;
import de.avatic.lcc.dto.generic.NodeType;
import de.avatic.lcc.dto.configuration.nodes.view.NodeDetailDTO;
import de.avatic.lcc.model.nodes.Node;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.service.transformer.generic.CountryTransformer;
import de.avatic.lcc.service.transformer.generic.LocationTransformer;
import de.avatic.lcc.service.transformer.generic.NodeTransformer;
@ -32,13 +32,17 @@ public class NodeDetailTransformer {
this.nodeRepository = nodeRepository;
}
public NodeDetailDTO toNodeViewDTO(Node node) {
public NodeDetailDTO toNodeDetailDTO(Node node) {
NodeDetailDTO dto = new NodeDetailDTO();
Map<Integer, NodeDTO> predecessors = new HashMap<>();
var chains = new ArrayList<Map<Integer, NodeDTO>>();
for (Integer seq : node.getNodePredecessors().keySet())
predecessors.put(seq, nodeTransformer.toNodeDTO(nodeRepository.getById(node.getNodePredecessors().get(seq)).orElseThrow()));
for (var chain : node.getNodePredecessors()) {
Map<Integer, NodeDTO> predecessorChain = new HashMap<>();
for (Integer seq : chain.keySet())
predecessorChain.put(seq, nodeTransformer.toNodeDTO(nodeRepository.getById(chain.get(seq)).orElseThrow()));
chains.add(predecessorChain);
}
dto.setId(node.getId());
dto.setDeprecated(node.getDeprecated());
@ -47,7 +51,7 @@ public class NodeDetailTransformer {
dto.setAddress(node.getAddress());
dto.setLocation(locationTransformer.toLocationDTO(node));
dto.setTypes(toNodeTypeArrayList(node));
dto.setPredecessors(predecessors);
dto.setPredecessors(chains);
dto.setOutboundCountries(node.getOutboundCountries().stream().map(id -> countryTransformer.toCountryDTO(countryRepository.getById(id).orElseThrow())).toList());
return dto;
@ -55,7 +59,7 @@ public class NodeDetailTransformer {
private ArrayList<NodeType> toNodeTypeArrayList(Node entity) {
ArrayList<NodeType> types = new ArrayList<>();
if (entity.getSink()) types.add(NodeType.DESTINATION);
if (entity.getDestination()) types.add(NodeType.DESTINATION);
if (entity.getSource()) types.add(NodeType.SOURCE);
if (entity.getIntermediate()) types.add(NodeType.INTERMEDIATE);
return types;

View file

@ -6,3 +6,4 @@ spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.sql.init.mode=always
#spring.profiles.active=setup
lcc.bulk.sheet_password=secretSheet?!

View file

@ -42,10 +42,10 @@ CREATE TABLE IF NOT EXISTS `system_property`
-- country
CREATE TABLE IF NOT EXISTS `country`
(
`id` INT NOT NULL AUTO_INCREMENT,
`iso_code` CHAR(2) NOT NULL COMMENT 'ISO 3166-1 alpha-2 country code',
`region_code` CHAR(5) NOT NULL COMMENT 'Geographic region code (EMEA/LATAM/APAC/NAM)',
`is_deprecated` BOOLEAN NOT NULL DEFAULT FALSE,
`id` INT NOT NULL AUTO_INCREMENT,
`iso_code` CHAR(2) NOT NULL COMMENT 'ISO 3166-1 alpha-2 country code',
`region_code` CHAR(5) NOT NULL COMMENT 'Geographic region code (EMEA/LATAM/APAC/NAM)',
`is_deprecated` BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_country_iso_code` (`iso_code`),
CONSTRAINT `chk_country_region_code`
@ -138,7 +138,7 @@ CREATE TABLE IF NOT EXISTS node
address VARCHAR(500) NOT NULL,
external_mapping_id VARCHAR(32),
predecessor_required BOOLEAN NOT NULL DEFAULT FALSE,
is_destination BOOLEAN,
is_destination BOOLEAN,
is_source BOOLEAN,
is_intermediate BOOLEAN,
geo_lat DECIMAL(7, 4) CHECK (geo_lat BETWEEN -90 AND 90),
@ -149,16 +149,24 @@ CREATE TABLE IF NOT EXISTS node
INDEX idx_country_id (country_id)
) COMMENT '';
CREATE TABLE IF NOT EXISTS node_predecessor
CREATE TABLE IF NOT EXISTS node_predecessor_chain
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
node_id INT NOT NULL,
predecessor_node_id INT NOT NULL,
sequence_number INT NOT NULL CHECK (sequence_number > 0),
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
node_id INT NOT NULL,
FOREIGN KEY (node_id) REFERENCES node (id)
);
CREATE TABLE IF NOT EXISTS node_predecessor_entry
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
node_id INT NOT NULL,
node_predecessor_chain_id INT NOT NULL,
sequence_number INT NOT NULL CHECK (sequence_number > 0),
FOREIGN KEY (node_id) REFERENCES node (id),
FOREIGN KEY (predecessor_node_id) REFERENCES node (id),
UNIQUE KEY uk_node_predecessor (node_id, predecessor_node_id, sequence_number),
INDEX idx_predecessor_node (predecessor_node_id),
FOREIGN KEY (node_predecessor_chain_id) REFERENCES node_predecessor_chain (id),
UNIQUE KEY uk_node_predecessor (node_predecessor_chain_id, sequence_number),
INDEX idx_node_predecessor (node_predecessor_chain_id),
INDEX idx_sequence (sequence_number)
);
@ -210,9 +218,9 @@ CREATE TABLE IF NOT EXISTS container_rate
from_node_id INT NOT NULL,
to_node_id INT NOT NULL,
container_rate_type CHAR(8) CHECK (container_rate_type IN ('RAIL', 'SEA', 'POST-RUN', 'ROAD')),
rate_teu DECIMAL(15, 2) NOT NULL COMMENT 'rate for 20ft container in EUR',
rate_feu DECIMAL(15, 2) NOT NULL COMMENT 'rate for 40ft container in EUR',
rate_hc DECIMAL(15, 2) NOT NULL COMMENT 'rate for 40ft HQ container in EUR',
rate_teu DECIMAL(15, 2) NOT NULL COMMENT 'rate for 20ft container in EUR',
rate_feu DECIMAL(15, 2) NOT NULL COMMENT 'rate for 40ft container in EUR',
rate_hc DECIMAL(15, 2) NOT NULL COMMENT 'rate for 40ft HQ container in EUR',
lead_time INT UNSIGNED NOT NULL COMMENT 'lead time in days',
validity_period_id INT NOT NULL,
FOREIGN KEY (from_node_id) REFERENCES node (id),
@ -290,12 +298,12 @@ CREATE TABLE IF NOT EXISTS packaging
CREATE TABLE IF NOT EXISTS packaging_property_type
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
external_mapping_id VARCHAR(16) NOT NULL,
`data_type` VARCHAR(16),
`validation_rule` VARCHAR(64),
`is_required` BOOLEAN NOT NULL DEFAULT FALSE,
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
external_mapping_id VARCHAR(16) NOT NULL,
`data_type` VARCHAR(16),
`validation_rule` VARCHAR(64),
`is_required` BOOLEAN NOT NULL DEFAULT FALSE,
UNIQUE KEY idx_packaging_property_type (`external_mapping_id`),
CONSTRAINT `chk_packaging_data_type_values` CHECK (`data_type` IN
('INT', 'PERCENTAGE', 'BOOLEAN', 'CURRENCY', 'ENUMERATION',
@ -361,13 +369,13 @@ CREATE TABLE IF NOT EXISTS premiss
CREATE TABLE IF NOT EXISTS premiss_destination
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
premiss_id INT NOT NULL,
annual_amount INT UNSIGNED NOT NULL COMMENT 'annual amount in single pieces',
destination_node_id INT NOT NULL,
repacking_cost DECIMAL(15, 2) DEFAULT NULL,
handling_cost DECIMAL(15, 2) DEFAULT NULL,
disposal_cost DECIMAL(15, 2) DEFAULT NULL,
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
premiss_id INT NOT NULL,
annual_amount INT UNSIGNED NOT NULL COMMENT 'annual amount in single pieces',
destination_node_id INT NOT NULL,
repacking_cost DECIMAL(15, 2) DEFAULT NULL,
handling_cost DECIMAL(15, 2) DEFAULT NULL,
disposal_cost DECIMAL(15, 2) DEFAULT NULL,
FOREIGN KEY (premiss_id) REFERENCES premiss (id),
FOREIGN KEY (destination_node_id) REFERENCES node (id),
INDEX idx_destination_node_id (destination_node_id),
@ -376,11 +384,11 @@ CREATE TABLE IF NOT EXISTS premiss_destination
CREATE TABLE IF NOT EXISTS premiss_route
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
premiss_destination_id INT NOT NULL,
is_fastest BOOLEAN DEFAULT FALSE,
is_cheapest BOOLEAN DEFAULT FALSE,
is_selected BOOLEAN DEFAULT FALSE,
is_fastest BOOLEAN DEFAULT FALSE,
is_cheapest BOOLEAN DEFAULT FALSE,
is_selected BOOLEAN DEFAULT FALSE,
FOREIGN KEY (premiss_destination_id) REFERENCES premiss_destination (id)
);
@ -391,7 +399,7 @@ CREATE TABLE IF NOT EXISTS premiss_route_node
user_node_id INT DEFAULT NULL,
name VARCHAR(255) NOT NULL,
address VARCHAR(500),
is_destination BOOLEAN DEFAULT FALSE,
is_destination BOOLEAN DEFAULT FALSE,
is_intermediate BOOLEAN DEFAULT FALSE,
is_source BOOLEAN DEFAULT FALSE,
geo_lat DECIMAL(7, 4) CHECK (geo_lat BETWEEN -90 AND 90),
@ -554,7 +562,7 @@ CREATE TABLE IF NOT EXISTS calculation_job_destination
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
calculation_job_id INT NOT NULL,
premiss_destination_id INT NOT NULL,
premiss_destination_id INT NOT NULL,
safety_stock INT UNSIGNED COMMENT 'safety stock in single pieces ?!?!',
shipping_frequency INT UNSIGNED COMMENT 'annual shipping frequency',
total_cost DECIMAL(15, 2) COMMENT 'aka MEK_B in EUR',