Added errors to database and simple error view in frontend

This commit is contained in:
Jan 2025-09-19 17:00:18 +02:00
parent e5bd56d3a9
commit 5e114ce859
53 changed files with 1827 additions and 185 deletions

View file

@ -18,31 +18,31 @@
</thead>
<transition name="list-container" mode="out-in">
<tbody v-if="data.length === 0">
<tr class="no-data">
<td :colspan="columns.length" class="no-data-cell">
{{ filter ? 'No results found' : 'No data available' }}
</td>
</tr>
<tr class="no-data">
<td :colspan="columns.length" class="no-data-cell">
{{ filter ? 'No results found' : 'No data available' }}
</td>
</tr>
</tbody>
<tbody v-else-if="loading">
<tr class="no-data">
<td :colspan="columns.length" class="no-data-cell">
<spinner class="space-around"></spinner>
</td>
</tr>
<tr class="no-data">
<td :colspan="columns.length" class="no-data-cell">
<spinner class="space-around"></spinner>
</td>
</tr>
</tbody>
<tbody v-else>
<tr v-for="(item, index) in data" :key="index" class="table-row">
<td v-for="column in columns" :key="column.key" class="table-cell" :class="getAlignment(column.align)">
<span v-if="column.iconResolver == null">{{ getCellValue(item, column) }}</span>
<component v-else
:is="getCellValue(item, column)"
weight="regular"
size="24"
class="table-icon"
/>
</td>
</tr>
<tr v-for="(item, index) in data" :key="index" class="table-row" @click="$emit('row-click', item)" :class="{'table-row--hover': mouseOver}">
<td v-for="column in columns" :key="column.key" class="table-cell" :class="getAlignment(column.align)">
<span v-if="column.iconResolver == null">{{ getCellValue(item, column) }}</span>
<component v-else
:is="getCellValue(item, column)"
weight="regular"
size="24"
class="table-icon"
/>
</td>
</tr>
</tbody>
</transition>
</table>
@ -66,11 +66,17 @@ import Pagination from "@/components/UI/Pagination.vue";
export default {
name: "TableView",
components: {Pagination, Box, SearchBar, BasicButton, Checkbox, Spinner},
emits: ['row-click'],
props: {
dataSource: {
type: Function,
required: true
},
mouseOver: {
type: Boolean,
default: false,
},
columns: {
type: Array,
required: true,
@ -195,7 +201,7 @@ export default {
overflow: visible;
border-radius: 0.8rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
background-color: #FFFFFF;
}
.loading {
@ -261,5 +267,9 @@ export default {
background-color: rgba(107, 134, 156, 0.05);
}
.table-row--hover {
cursor: pointer;
}
</style>

View file

@ -0,0 +1,127 @@
<template>
<div class="trace-view-container" :class="trace ? '' : 'trace-view-container--no-trace'">
<div class="trace-view-header">
<h3 class="sub-header">{{ title }} <br><span class="trace-view-message">{{ message }}</span>
</h3>
<icon-button icon="x" @click="$emit('close')"></icon-button>
</div>
<div v-if="trace" class="trace-view">
<div class="trace-view-exception">{{ exception }}</div>
<div :class="highlightClasses(traceItem)" v-for="traceItem of trace">{{ fullClassName(traceItem) }} (<span
class="trace-view-file">{{ fileName(traceItem) }}</span>)
</div>
</div>
</div>
</template>
<script>
import BasicButton from "@/components/UI/BasicButton.vue";
import IconButton from "@/components/UI/IconButton.vue";
import {PhWarning} from "@phosphor-icons/vue";
export default {
name: "ErrorLogModal",
components: {PhWarning, IconButton, BasicButton},
emits: ['close'],
props: {
error: {
type: Object,
required: true
}
},
computed: {
trace() {
return this.error.trace;
},
exception() {
return this.error.code;
},
title() {
return this.error.title;
},
code() {
return this.error.code;
},
message() {
return this.error.message;
},
},
created() {
console.log(this.error);
console.log(this.error.trace);
},
methods: {
highlightClasses(traceItem) {
return traceItem.fullPath?.includes('de.avatic.lcc') ? 'highlight-trace-item' : 'trace-item';
},
fullClassName(traceItem) {
return traceItem.fullPath;
},
fileName(traceItem) {
return `${traceItem.file}:${traceItem.line}`;
},
}
}
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=Cal+Sans&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
.highlight-trace-item {
color: #002F54;
background-color: #F2F2F2;
padding-left: 1.6rem;
}
.trace-item {
padding-left: 1.6rem;
}
.trace-view {
overflow: auto;
font-family: Roboto Mono, monospace;
font-size: 1.2rem;
flex: 1;
padding: 0 1.6rem;
}
.trace-view-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.trace-view-message {
font-size: 1.2rem;
}
.trace-view-file {
font-weight: 500;
text-decoration: underline;
}
.trace-view-exception {
}
.trace-view-container {
height: 90vh;
width: 80vw;
display: flex;
flex-direction: column;
gap: 1.6rem;
}
.trace-view-container--no-trace {
height: auto;
width: auto;
}
</style>

View file

@ -59,11 +59,6 @@
<radio-option name="import-dataset" value="MATERIAL" v-model="importDataset">materials</radio-option>
<radio-option name="import-dataset" value="PACKAGING" v-model="importDataset">packaging</radio-option>
</div>
<div class="bulk-operation-caption">import mode</div>
<div class="bulk-operation-data">
<radio-option name="import-type" value="APPEND" v-model="importType">append existing data</radio-option>
<radio-option name="import-type" value="FULL" v-model="importType">fully replace data</radio-option>
</div>
<div class="bulk-operation-caption">file</div>
<div class="bulk-operation-data">
@ -111,7 +106,6 @@ export default {
exportType: "templates",
exportDataset: "NODE",
importDataset: "NODE",
importType: "APPEND",
selectedFileName: null,
selectedFile: null,
uploading: false,
@ -177,8 +171,8 @@ export default {
if (!this.selectedFile)
return;
const url = `${config.backendUrl}/bulk/upload/${this.importDataset}/${this.importType}/`
this.processId = await performUpload(url, this.selectedFile);
const url = `${config.backendUrl}/bulk/upload/${this.importDataset}/`
this.processId = await performUpload(url, this.selectedFile).catch(error => {});
}
}
}

View file

@ -0,0 +1,67 @@
<template>
<div>
<h2 class="page-header">Errors</h2>
<modal :state="showModal">
<error-log-modal :error="error" @close="showModal = false"></error-log-modal>
</modal>
<table-view :columns="columns" :data-source="fetchData" :page="pagination.page" :page-size="pageSize" :page-count="pagination.pageCount" :total-count="pagination.totalCount" @row-click="showDetails" :mouse-over="true"></table-view>
</div>
</template>
<script>
import TableView from "@/components/UI/TableView.vue";
import {useErrorLogStore} from "@/store/errorLog.js";
import {mapStores} from "pinia";
import Modal from "@/components/UI/Modal.vue";
import TraceView from "@/components/layout/TraceView.vue";
import ErrorLogModal from "@/components/layout/ErrorLogModal.vue";
export default {
name: "ErrorLog",
components: {ErrorLogModal, TraceView, Modal, TableView},
data() {
return {
showModal: false,
error: null,
pageSize: 10,
pagination: { page: 1, pageCount: 10, totalCount: 1 },
columns: [
{key: 'type', label: 'Type'},
{key: 'title', label: 'Title'},
{key: 'message', label: 'Message'},
{key: 'code', label: 'Exception'},
{key: 'timestamp', label: 'Timestamp', formatter: (value) => this.buildDate(value) },
{key: 'user_id', label: 'User'},
],
}
},
computed: {
...mapStores(useErrorLogStore)
},
methods: {
async fetchData(query) {
await this.errorLogStore.setQuery(query);
this.pagination = this.errorLogStore.getPagination;
return this.errorLogStore.getErrors;
},
buildDate(date) {
return `${date[0]}-${date[1].toString().padStart(2, '0')}-${date[2].toString().padStart(2, '0')} ${date[3].toString().padStart(2, '0')}:${date[4].toString().padStart(2, '0')}:${date[5].toString().padStart(2, '0')}`
},
showDetails(error) {
console.log("click")
this.error = error;
this.showModal = true;
}
}
}
</script>
<style scoped>
</style>

View file

@ -5,6 +5,7 @@ import Config from "@/pages/Config.vue";
import CalculationSingleEdit from "@/pages/CalculationSingleEdit.vue";
import CalculationMassEdit from "@/pages/CalculationMassEdit.vue";
import CalculationAssistant from "@/pages/CalculationAssistant.vue";
import ErrorLog from "@/pages/ErrorLog.vue";
const router = createRouter({
history: createWebHistory(),
@ -48,6 +49,11 @@ const router = createRouter({
path: '/config',
component: Config
},
{
path: '/error',
component: ErrorLog
},
{
path: '/:pathMatch(.*)*',
redirect: '/calculations'

View file

@ -36,6 +36,8 @@ export const useErrorStore = defineStore('error', {
timestamp: Date.now()
}
console.log(error);
this.errors.push(error);
this.sendCache.push(error);
await this.transmitErrors();
@ -52,6 +54,7 @@ export const useErrorStore = defineStore('error', {
const url = `${config.backendUrl}/error/`;
const response = await fetch(url, params
).catch(e => {
this.startAutoSubmitTimer();
@ -110,7 +113,7 @@ export const useErrorStore = defineStore('error', {
return storeState;
},
async submitOnBeforeUnload() {
if (this.errors.length > 0) {
if (this.sendCache.length > 0) {
navigator.sendBeacon('/api/errors', JSON.stringify(
toRaw(this.sendCache)
))
@ -125,29 +128,29 @@ export const useErrorStore = defineStore('error', {
export function setupErrorBuffer() {
const errorStore = useErrorStore()
// // Unhandled Promise Rejections
// window.addEventListener('unhandledrejection', (event) => {
//
// const error = {
// code: "Unhandled rejection",
// title: "Frontend error",
// message: event.reason?.message || 'Unhandled Promise Rejection',
// traceCombined: event.reason?.stack,
// };
//
// errorStore.addError(error, {global: true}).then(r => {} );
// })
//
// // JavaScript Errors
// window.addEventListener('error', (event) => {
// const error = {
// code: "General error",
// title: "Frontend error",
// message: event.reason?.message || 'Unhandled Promise Rejection',
// traceCombined: event.reason?.stack,
// };
// errorStore.addError(error, {global: true}).then(r => {} );
// })
//Unhandled Promise Rejections
window.addEventListener('unhandledrejection', (event) => {
const error = {
code: "Unhandled rejection",
title: "Frontend error",
message: event.reason?.message || 'Unhandled Promise Rejection',
traceCombined: event.reason?.stack,
};
errorStore.addError(error, {global: true}).then(r => {} );
})
// JavaScript Errors
window.addEventListener('error', (event) => {
const error = {
code: "General error",
title: "Frontend error",
message: event.reason?.message || 'Unhandled Promise Rejection',
traceCombined: event.reason?.stack,
};
errorStore.addError(error, {global: true}).then(r => {} );
})
window.addEventListener('beforeunload', () => {
errorStore.submitOnBeforeUnload();

View file

@ -0,0 +1,60 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import performRequest from "@/backend.js";
export const useErrorLogStore = defineStore('errorLog', {
state() {
return {
errors: [],
loading: false,
query: {},
pagination: {}
}
},
getters: {
isLoading(state) {
return state.loading;
},
getErrors(state) {
return state.errors;
},
getPagination(state) {
return state.pagination;
}
},
actions: {
async setQuery(query = {}) {
this.query = query;
await this.updateErrors();
},
async updateErrors() {
this.loading = true;
this.errors = [];
const params = new URLSearchParams();
if (this.query?.searchTerm && this.query.searchTerm !== '')
params.append('filter', this.query.searchTerm);
if(this.query?.page)
params.append('page', this.query.page);
if(this.query?.pageSize)
params.append('limit', this.query.pageSize);
const url = `${config.backendUrl}/error/${params.size === 0 ? '' : '?'}${params.toString()}`;
const {data: data, headers: headers} = await performRequest(this, "GET", url, null);
this.errors = data;
this.pagination = { page: parseInt(headers.get('X-Current-Page')), pageCount: parseInt(headers.get('X-Page-Count')), totalCount: parseInt(headers.get('X-Total-Count'))};
console.log(this.pagination)
this.loading = false;
}
}
});

View file

@ -1,21 +1,42 @@
package de.avatic.lcc.controller;
import de.avatic.lcc.dto.error.ErrorLogDTO;
import de.avatic.lcc.dto.error.FrontendErrorDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import de.avatic.lcc.service.error.SysErrorService;
import jakarta.validation.constraints.Min;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping({"/api/error", "/api/error/"})
public class ErrorController {
private final SysErrorService sysErrorService;
public ErrorController(SysErrorService sysErrorService) {
this.sysErrorService = sysErrorService;
}
@PostMapping
public void error(@RequestBody List<FrontendErrorDTO> error) {
System.out.println(error.toString());
//TODO store in database.
public void error(@RequestBody List<FrontendErrorDTO> errors) {
sysErrorService.addErrors(errors);
}
@GetMapping
public ResponseEntity<List<ErrorLogDTO>> listErrors(@RequestParam(defaultValue = "20") @Min(1) int limit,
@RequestParam(defaultValue = "1") @Min(1) int page,
@RequestParam(required = false) Optional<String> filter) {
SearchQueryResult<ErrorLogDTO> errors = sysErrorService.listErrors(filter, page, limit);
return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(errors.getTotalElements()))
.header("X-Page-Count", String.valueOf(errors.getTotalPages()))
.header("X-Current-Page", String.valueOf(errors.getPage()))
.body(errors.toList());
}
}

View file

@ -1,9 +1,9 @@
package de.avatic.lcc.controller.bulk;
import com.azure.core.annotation.BodyParam;
import de.avatic.lcc.dto.bulk.BulkFileType;
import de.avatic.lcc.dto.bulk.BulkProcessingType;
import de.avatic.lcc.dto.bulk.BulkStatus;
import de.avatic.lcc.dto.bulk.BulkStatusDTO;
import de.avatic.lcc.service.bulk.BulkExportService;
import de.avatic.lcc.service.bulk.BulkFileProcessingService;
import de.avatic.lcc.service.bulk.TemplateExportService;
@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
/**
* REST controller for handling bulk operations, including file uploads, template generation,
@ -36,27 +37,25 @@ public class BulkOperationController {
}
/**
* Retrieves the current status of a specific bulk processing operation.
* Retrieves the current status of all bulk processing operations.
*
* @param id The unique identifier of the operation (processing_id) to check its status.
* @return A ResponseEntity with the bulk processing status payload.
*/
@GetMapping({"/status/{processing_id}","/status/{processing_id}/"})
public ResponseEntity<BulkStatus> getUploadStatus(@PathVariable("processing_id") Integer id) {
return ResponseEntity.ok(bulkProcessingService.getStatus(id));
@GetMapping({"/status/","/status"})
public ResponseEntity<List<BulkStatusDTO>> getUploadStatus() {
return ResponseEntity.ok(bulkProcessingService.getStatus());
}
/**
* Handles the upload of a file for a specific processing type and file type.
*
* @param processingType The processing type ("full" or "append") associated with the upload.
* @param type The file type being uploaded, as defined in {@link BulkFileType}.
* @param file The file to be uploaded, provided as a multipart file.
* @return A ResponseEntity indicating whether the upload was processed successfully.
*/
@PostMapping({"/upload/{type}/{processing_type}","/upload/{type}/{processing_type}/"})
public ResponseEntity<Integer> uploadFile(@PathVariable("processing_type") BulkProcessingType processingType, @PathVariable BulkFileType type, @RequestParam("file") MultipartFile file) {
return ResponseEntity.ok(bulkProcessingService.processFile(type, processingType, file));
@PostMapping({"/upload/{type}","/upload/{type}/"})
public ResponseEntity<Integer> uploadFile(@PathVariable BulkFileType type, @BodyParam("file") MultipartFile file) {
return ResponseEntity.ok(bulkProcessingService.processFile(type, file));
}
/**
@ -89,7 +88,7 @@ public class BulkOperationController {
* @throws IllegalArgumentException if the provided file type is not supported.
*/
@GetMapping({"/download/{type}", "/download/{type}/"})
public ResponseEntity<InputStreamResource> downloadFile(@PathVariable BulkFileType type) throws IOException {
public ResponseEntity<InputStreamResource> scheduleDownload(@PathVariable BulkFileType type) throws IOException {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename=lcc_export_" + type.name().toLowerCase() + ".xlsx");
@ -110,7 +109,7 @@ public class BulkOperationController {
* @throws IllegalArgumentException if the file type or validity period ID is invalid.
*/
@GetMapping({"/download/{type}/{validity_period_id}","/download/{type}/{validity_period_id}/"})
public ResponseEntity<InputStreamResource> downloadFile(@PathVariable BulkFileType type, @PathVariable("validity_period_id") Integer validityPeriodId) throws IOException {
public ResponseEntity<InputStreamResource> scheduleDownload(@PathVariable BulkFileType type, @PathVariable("validity_period_id") Integer validityPeriodId) throws IOException {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename=lcc_export_" + type.name().toLowerCase() + ".xlsx");
@ -120,4 +119,20 @@ public class BulkOperationController {
.contentType(MediaType.parseMediaType("application/vnd.ms-excel"))
.body(new InputStreamResource(bulkExportService.generateExport(BulkFileType.valueOf(type.name().toUpperCase()), validityPeriodId)));
}
// @GetMapping({"/file/{processId}","/file/{processId}/"})
// public ResponseEntity<InputStreamResource> download(@PathVariable Integer processId) throws IOException {
// bulkExportService.export(processId);
//
// HttpHeaders headers = new HttpHeaders();
// headers.add("Content-Disposition", "attachment; filename=lcc_export_" + type.name().toLowerCase() + ".xlsx");
//
//
// return ResponseEntity
// .ok()
// .headers(headers)
// .contentType(MediaType.parseMediaType("application/vnd.ms-excel"))
// .body(new InputStreamResource(bulkExportService.generateExport(BulkFileType.valueOf(type.name().toUpperCase()), validityPeriodId)));
// }
}

View file

@ -1,5 +1,5 @@
package de.avatic.lcc.dto.bulk;
public enum BulkProcessingType {
FULL, APPEND
UPLOAD, DOWNLOAD
}

View file

@ -0,0 +1,5 @@
package de.avatic.lcc.dto.bulk;
public enum BulkState {
QUEUED, PROCESSING, COMPLETED, FAILED
}

View file

@ -1,4 +0,0 @@
package de.avatic.lcc.dto.bulk;
public class BulkStatus {
}

View file

@ -0,0 +1,12 @@
package de.avatic.lcc.dto.bulk;
public class BulkStatusDTO {
private BulkFileType operation;
private int processingId;
private BulkState state;
}

View file

@ -11,6 +11,7 @@ public class ErrorDTO {
private String title;
private String message;
private List<StackTraceElement> trace;
private String traceCombined;
public ErrorDTO() {
}
@ -79,4 +80,11 @@ public class ErrorDTO {
return Objects.hash(code, message, trace);
}
public String getTraceCombined() {
return traceCombined;
}
public void setTraceCombined(String traceCombined) {
this.traceCombined = traceCombined;
}
}

View file

@ -0,0 +1,112 @@
package de.avatic.lcc.dto.error;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDateTime;
import java.util.List;
public class ErrorLogDTO {
@JsonProperty("user_id")
private Integer userId;
private String code;
private String title;
private String message;
private String pinia;
@JsonProperty("calculation_job_id")
private Integer calculationJobId;
@JsonProperty("bulk_operation_id")
private Integer bulkOperationId;
private String type;
private List<ErrorLogTraceItemDto> trace;
@JsonProperty("timestamp")
private LocalDateTime createdAt;
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getPinia() {
return pinia;
}
public void setPinia(String pinia) {
this.pinia = pinia;
}
public Integer getCalculationJobId() {
return calculationJobId;
}
public void setCalculationJobId(Integer calculationJobId) {
this.calculationJobId = calculationJobId;
}
public Integer getBulkOperationId() {
return bulkOperationId;
}
public void setBulkOperationId(Integer bulkOperationId) {
this.bulkOperationId = bulkOperationId;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public List<ErrorLogTraceItemDto> getTrace() {
return trace;
}
public void setTrace(List<ErrorLogTraceItemDto> trace) {
this.trace = trace;
}
@JsonProperty("timestamp")
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}

View file

@ -0,0 +1,41 @@
package de.avatic.lcc.dto.error;
public class ErrorLogTraceItemDto {
Integer line;
String file;
String method;
String fullPath;
public Integer getLine() {
return line;
}
public void setLine(Integer line) {
this.line = line;
}
public String getFile() {
return file;
}
public void setFile(String file) {
this.file = file;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getFullPath() {
return fullPath;
}
public void setFullPath(String fullPath) {
this.fullPath = fullPath;
}
}

View file

@ -0,0 +1,29 @@
package de.avatic.lcc.model.bulk;
public class BulkOperation<T> {
private BulkOperationType type;
private T entity;
public BulkOperation(T entity, BulkOperationType type) {
this.entity = entity;
this.type = type;
}
public BulkOperationType getType() {
return type;
}
public void setType(BulkOperationType type) {
this.type = type;
}
public T getEntity() {
return entity;
}
public void setEntity(T entity) {
this.entity = entity;
}
}

View file

@ -0,0 +1,5 @@
package de.avatic.lcc.model.bulk;
public enum BulkOperationType {
UPDATE, DELETE
}

View file

@ -0,0 +1,71 @@
package de.avatic.lcc.model.bulk;
import de.avatic.lcc.dto.bulk.BulkFileType;
import de.avatic.lcc.dto.bulk.BulkProcessingType;
import de.avatic.lcc.dto.bulk.BulkState;
import org.apache.commons.compress.parallel.InputStreamSupplier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
public class BulkProcess {
private int id;
private BulkState state;
private BulkFileType type;
private BulkProcessingType processingType;
private MultipartFile bulkRequest;
private ResponseEntity<InputStreamSupplier> bulkResponse;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public BulkState getState() {
return state;
}
public void setState(BulkState state) {
this.state = state;
}
public BulkFileType getType() {
return type;
}
public void setType(BulkFileType type) {
this.type = type;
}
public BulkProcessingType getProcessingType() {
return processingType;
}
public void setProcessingType(BulkProcessingType processingType) {
this.processingType = processingType;
}
public MultipartFile getBulkRequest() {
return bulkRequest;
}
public void setBulkRequest(MultipartFile bulkRequest) {
this.bulkRequest = bulkRequest;
}
public ResponseEntity getBulkResponse() {
return bulkResponse;
}
public void setBulkResponse(ResponseEntity bulkResponse) {
this.bulkResponse = bulkResponse;
}
}

View file

@ -0,0 +1,5 @@
package de.avatic.lcc.model.bulk;
public enum BulkProcessState {
QUEUED, PROCESSING, COMPLETED, FAILED
}

View file

@ -1,4 +1,4 @@
package de.avatic.lcc.model.bulk;
package de.avatic.lcc.model.bulk.header;
public enum ContainerRateHeader implements HeaderProvider {
FROM_NODE("Origin"), TO_NODE("Destination"), CONTAINER_RATE_TYPE("Transport mode"), RATE_FEU("Rate 40 ft GP [EUR]"),

View file

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

View file

@ -1,6 +1,6 @@
package de.avatic.lcc.model.bulk;
package de.avatic.lcc.model.bulk.header;
public enum HiddenCountryHeader implements HeaderProvider{
public enum HiddenCountryHeader implements HeaderProvider {
ISO_CODE("Iso Code"), NAME("Name");
private final String header;

View file

@ -1,4 +1,4 @@
package de.avatic.lcc.model.bulk;
package de.avatic.lcc.model.bulk.header;
public enum HiddenNodeHeader implements HeaderProvider {
MAPPING_ID("Mapping Id"), NAME("Name");

View file

@ -1,6 +1,7 @@
package de.avatic.lcc.model.bulk;
package de.avatic.lcc.model.bulk.header;
public enum MaterialHeader implements HeaderProvider {
OPERATION("Operation"),
PART_NUMBER("Part number"),
DESCRIPTION("Material description"),
HS_CODE("HS code");

View file

@ -1,4 +1,4 @@
package de.avatic.lcc.model.bulk;
package de.avatic.lcc.model.bulk.header;
public enum MatrixRateHeader implements HeaderProvider {
FROM_COUNTRY("Country of origin (ISO 3166-1)"),

View file

@ -1,7 +1,7 @@
package de.avatic.lcc.model.bulk;
package de.avatic.lcc.model.bulk.header;
public enum NodeHeader implements HeaderProvider {
MAPPING_ID("Mapping ID"), NAME("Name"), ADDRESS("Address"),
OPERATION("Operation"), MAPPING_ID("Mapping ID"), NAME("Name"), ADDRESS("Address"),
COUNTRY("Country (ISO 3166-1)"), GEO_LATITUDE("Latitude"), GEO_LONGITUDE("Longitude"),
IS_SOURCE("Source"), IS_INTERMEDIATE("Intermediate"), IS_DESTINATION("Destination"),
OUTBOUND_COUNTRIES("Outbound countries (ISO 3166-1)"), PREDECESSOR_NODES("Predecessor Nodes (Mapping ID)"),
@ -9,11 +9,15 @@ public enum NodeHeader implements HeaderProvider {
private final String header;
NodeHeader(String header) {
this.header = header;
}
@Override
public String getHeader() {
return header;
}
}

View file

@ -1,6 +1,7 @@
package de.avatic.lcc.model.bulk;
package de.avatic.lcc.model.bulk.header;
public enum PackagingHeader implements HeaderProvider {
OPERATION("Operation"),
PART_NUMBER("Part number"),
SUPPLIER("Supplier (Mapping ID)"),
SHU_LENGTH("SHU length"),
@ -16,8 +17,7 @@ public enum PackagingHeader implements HeaderProvider {
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");
HU_UNIT_COUNT("Units/HU [pieces]");
private final String header;

View file

@ -0,0 +1,252 @@
package de.avatic.lcc.model.error;
import java.time.LocalDateTime;
import java.util.List;
/**
* Represents a system error with detailed information about the error occurrence,
* including user context, error type, and stack trace.
*/
public class SysError {
/**
* The unique identifier of the system error.
*/
private Integer id;
/**
* The identifier of the user associated with this error.
*/
private Integer userId;
/**
* The title or brief description of the error.
*/
private String title;
/**
* The detailed error message.
*/
private String message;
/**
* The error code identifier.
*/
private String code;
/**
* The Pinia store state related to this error.
*/
private String pinia;
/**
* The type classification of the system error.
*/
private SysErrorType type;
/**
* The identifier of the calculation job associated with this error.
*/
private Integer calculationJobId;
/**
* The identifier of the bulk operation associated with this error.
*/
private Integer bulkOperationId;
/**
* The list of stack trace items providing error location details.
*/
private List<SysErrorTraceItem> trace;
private LocalDateTime createdAt;
/**
* Gets the unique identifier of the system error.
*
* @return the error ID
*/
public Integer getId() {
return id;
}
/**
* Sets the unique identifier of the system error.
*
* @param id the error ID to set
*/
public void setId(Integer id) {
this.id = id;
}
/**
* Gets the identifier of the user associated with this error.
*
* @return the user ID
*/
public Integer getUserId() {
return userId;
}
/**
* Sets the identifier of the user associated with this error.
*
* @param userId the user ID to set
*/
public void setUserId(Integer userId) {
this.userId = userId;
}
/**
* Gets the title or brief description of the error.
*
* @return the error title
*/
public String getTitle() {
return title;
}
/**
* Sets the title or brief description of the error.
*
* @param title the error title to set
*/
public void setTitle(String title) {
this.title = title;
}
/**
* Gets the detailed error message.
*
* @return the error message
*/
public String getMessage() {
return message;
}
/**
* Sets the detailed error message.
*
* @param message the error message to set
*/
public void setMessage(String message) {
this.message = message;
}
/**
* Gets the error code identifier.
*
* @return the error code
*/
public String getCode() {
return code;
}
/**
* Sets the error code identifier.
*
* @param code the error code to set
*/
public void setCode(String code) {
this.code = code;
}
/**
* Gets the Pinia store state related to this error.
*
* @return the Pinia store state
*/
public String getPinia() {
return pinia;
}
/**
* Sets the Pinia store identifier related to this error.
*
* @param pinia the Pinia store identifier to set
*/
public void setPinia(String pinia) {
this.pinia = pinia;
}
/**
* Gets the type classification of the system error.
*
* @return the error type
*/
public SysErrorType getType() {
return type;
}
/**
* Sets the type classification of the system error.
*
* @param type the error type to set
*/
public void setType(SysErrorType type) {
this.type = type;
}
/**
* Gets the identifier of the calculation job associated with this error.
*
* @return the calculation job ID
*/
public Integer getCalculationJobId() {
return calculationJobId;
}
/**
* Sets the identifier of the calculation job associated with this error.
*
* @param calculationJobId the calculation job ID to set
*/
public void setCalculationJobId(Integer calculationJobId) {
this.calculationJobId = calculationJobId;
}
/**
* Gets the identifier of the bulk operation associated with this error.
*
* @return the bulk operation ID
*/
public Integer getBulkOperationId() {
return bulkOperationId;
}
/**
* Sets the identifier of the bulk operation associated with this error.
*
* @param bulkOperationId the bulk operation ID to set
*/
public void setBulkOperationId(Integer bulkOperationId) {
this.bulkOperationId = bulkOperationId;
}
/**
* Gets the list of stack trace items providing error location details.
*
* @return the list of trace items
*/
public List<SysErrorTraceItem> getTrace() {
return trace;
}
/**
* Sets the list of stack trace items providing error location details.
*
* @param trace the list of trace items to set
*/
public void setTrace(List<SysErrorTraceItem> trace) {
this.trace = trace;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}

View file

@ -0,0 +1,145 @@
package de.avatic.lcc.model.error;
/**
* Represents a trace item in system error stack trace.
* Contains information about the location and context of an error occurrence.
*/
public class SysErrorTraceItem {
/**
* The unique identifier of the trace item.
*/
private Integer id;
/**
* The identifier of the associated system error.
*/
private Integer errorId;
/**
* The line number where the error occurred.
*/
private Integer line;
/**
* The file name where the error occurred.
*/
private String file;
/**
* The method name where the error occurred.
*/
private String method;
/**
* The full path to the file where the error occurred.
*/
private String fullPath;
/**
* Gets the unique identifier of the trace item.
*
* @return the trace item ID
*/
public Integer getId() {
return id;
}
/**
* Sets the unique identifier of the trace item.
*
* @param id the trace item ID to set
*/
public void setId(Integer id) {
this.id = id;
}
/**
* Gets the identifier of the associated system error.
*
* @return the error ID
*/
public Integer getErrorId() {
return errorId;
}
/**
* Sets the identifier of the associated system error.
*
* @param errorId the error ID to set
*/
public void setErrorId(Integer errorId) {
this.errorId = errorId;
}
/**
* Gets the line number where the error occurred.
*
* @return the line number
*/
public Integer getLine() {
return line;
}
/**
* Sets the line number where the error occurred.
*
* @param line the line number to set
*/
public void setLine(Integer line) {
this.line = line;
}
/**
* Gets the file name where the error occurred.
*
* @return the file name
*/
public String getFile() {
return file;
}
/**
* Sets the file name where the error occurred.
*
* @param file the file name to set
*/
public void setFile(String file) {
this.file = file;
}
/**
* Gets the method name where the error occurred.
*
* @return the method name
*/
public String getMethod() {
return method;
}
/**
* Sets the method name where the error occurred.
*
* @param method the method name to set
*/
public void setMethod(String method) {
this.method = method;
}
/**
* Gets the full path to the file where the error occurred.
*
* @return the full path
*/
public String getFullPath() {
return fullPath;
}
/**
* Sets the full path to the file where the error occurred.
*
* @param fullPath the full path to set
*/
public void setFullPath(String fullPath) {
this.fullPath = fullPath;
}
}

View file

@ -0,0 +1,5 @@
package de.avatic.lcc.model.error;
public enum SysErrorType {
BACKEND, FRONTEND, BULK, CALCULATION
}

View file

@ -0,0 +1,7 @@
package de.avatic.lcc.repositories.bulk;
import org.springframework.stereotype.Repository;
@Repository
public class BulkOperationRepository {
}

View file

@ -0,0 +1,178 @@
package de.avatic.lcc.repositories.error;
import de.avatic.lcc.model.error.SysError;
import de.avatic.lcc.model.error.SysErrorTraceItem;
import de.avatic.lcc.model.error.SysErrorType;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import jakarta.validation.constraints.Min;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.*;
import java.util.stream.Collectors;
@Repository
public class SysErrorRepository {
private final JdbcTemplate jdbcTemplate;
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public SysErrorRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
}
@Transactional
public void insert(List<SysError> errors) {
// First insert the sys_error records
String errorSql = "INSERT INTO sys_error (user_id, title, code, message, pinia, calculation_job_id, bulk_operation_id, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
for (SysError error : errors) {
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(errorSql, Statement.RETURN_GENERATED_KEYS);
ps.setObject(1, error.getUserId()); // Use setObject for nullable Integer
ps.setString(2, error.getTitle());
ps.setString(3, error.getCode());
ps.setString(4, error.getMessage());
ps.setString(5, error.getPinia());
ps.setObject(6, error.getCalculationJobId()); // Use setObject for nullable Integer
ps.setObject(7, error.getBulkOperationId()); // Use setObject for nullable Integer
ps.setString(8, error.getType().name());
return ps;
}, keyHolder);
// Get the generated error ID
Integer errorId = Objects.requireNonNull(keyHolder.getKey()).intValue();
// Insert trace items if they exist
if (error.getTrace() != null && !error.getTrace().isEmpty()) {
insertTraceItems(errorId, error.getTrace());
}
}
}
private void insertTraceItems(Integer errorId, List<SysErrorTraceItem> traceItems) {
String traceSql = "INSERT INTO sys_error_trace_item (error_id, line, file, method, fullPath) VALUES (?, ?, ?, ?, ?)";
jdbcTemplate.batchUpdate(traceSql, traceItems, traceItems.size(),
(ps, traceItem) -> {
ps.setInt(1, errorId);
ps.setObject(2, traceItem.getLine()); // Use setObject for nullable Integer
ps.setString(3, traceItem.getFile());
ps.setString(4, traceItem.getMethod());
ps.setString(5, traceItem.getFullPath());
});
}
@Transactional
public SearchQueryResult<SysError> listErrors(Optional<String> filter, SearchQueryPagination pagination) {
StringBuilder whereClause = new StringBuilder();
MapSqlParameterSource parameters = new MapSqlParameterSource();
// Build WHERE clause if filter is provided
if (filter.isPresent() && !filter.get().trim().isEmpty()) {
String filterValue = "%" + filter.get().trim() + "%";
whereClause.append(" WHERE (e.title LIKE :filter OR e.message LIKE :filter OR e.code LIKE :filter)");
parameters.addValue("filter", filterValue);
}
// Count total elements
String countSql = "SELECT COUNT(*) FROM sys_error e" + whereClause.toString();
Integer totalElements = namedParameterJdbcTemplate.queryForObject(countSql, parameters, Integer.class);
// Build main query with pagination
String sql = """
SELECT e.id, e.user_id, e.title, e.code, e.message, e.pinia,
e.calculation_job_id, e.bulk_operation_id, e.type, e.created_at
FROM sys_error e
""" + whereClause.toString() + """
ORDER BY e.created_at DESC
LIMIT :limit OFFSET :offset
""";
// Add pagination parameters
parameters.addValue("limit", pagination.getLimit());
parameters.addValue("offset", pagination.getOffset());
// Execute query
List<SysError> errors = namedParameterJdbcTemplate.query(sql, parameters, (rs, rowNum) -> {
SysError error = new SysError();
error.setId(rs.getInt("id"));
error.setUserId(rs.getObject("user_id", Integer.class));
error.setTitle(rs.getString("title"));
error.setCode(rs.getString("code"));
error.setMessage(rs.getString("message"));
error.setPinia(rs.getString("pinia"));
error.setCalculationJobId(rs.getObject("calculation_job_id", Integer.class));
error.setBulkOperationId(rs.getObject("bulk_operation_id", Integer.class));
error.setType(SysErrorType.valueOf(rs.getString("type")));
error.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
return error;
});
// Load trace items for each error
if (!errors.isEmpty()) {
loadTraceItemsForErrors(errors);
}
return new SearchQueryResult<>(errors, pagination.getPage(), totalElements, pagination.getLimit());
}
private void loadTraceItemsForErrors(List<SysError> errors) {
// Get all error IDs
List<Integer> errorIds = errors.stream()
.map(SysError::getId)
.toList();
if (errorIds.isEmpty()) {
return;
}
String traceSql = """
SELECT error_id, id, line, file, method, fullPath
FROM sys_error_trace_item
WHERE error_id IN (:errorIds)
ORDER BY error_id, id
""";
MapSqlParameterSource traceParameters = new MapSqlParameterSource("errorIds", errorIds);
// Query trace items
List<SysErrorTraceItem> allTraceItems = namedParameterJdbcTemplate.query(
traceSql,
traceParameters,
(rs, rowNum) -> {
SysErrorTraceItem traceItem = new SysErrorTraceItem();
traceItem.setErrorId(rs.getInt("error_id"));
traceItem.setId(rs.getInt("id"));
traceItem.setLine(rs.getObject("line", Integer.class));
traceItem.setFile(rs.getString("file"));
traceItem.setMethod(rs.getString("method"));
traceItem.setFullPath(rs.getString("fullPath"));
return traceItem;
}
);
// Group trace items by error ID
Map<Integer, List<SysErrorTraceItem>> traceItemsByErrorId = allTraceItems.stream()
.collect(Collectors.groupingBy(SysErrorTraceItem::getErrorId));
// Assign trace items to their respective errors
errors.forEach(error -> {
List<SysErrorTraceItem> traceItems = traceItemsByErrorId.getOrDefault(error.getId(), new ArrayList<>());
error.setTrace(traceItems);
});
}
}

View file

@ -1,11 +1,11 @@
package de.avatic.lcc.service.bulk;
import de.avatic.lcc.dto.bulk.BulkFileType;
import de.avatic.lcc.dto.bulk.BulkProcessingType;
import de.avatic.lcc.dto.bulk.BulkStatus;
import de.avatic.lcc.dto.bulk.BulkStatusDTO;
import de.avatic.lcc.model.bulk.BulkFileTypes;
import de.avatic.lcc.service.excelMapper.*;
import de.avatic.lcc.util.exception.badrequest.FileFormatNotSupportedException;
import de.avatic.lcc.util.exception.base.BadRequestException;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.List;
@Service
public class BulkFileProcessingService {
@ -22,9 +23,9 @@ public class BulkFileProcessingService {
private final MaterialExcelMapper materialExcelMapper;
private final PackagingExcelMapper packagingExcelMapper;
private final NodeExcelMapper nodeExcelMapper;
private final BulkStatusService bulkStatusService;
private final BulkProcessingService bulkStatusService;
public BulkFileProcessingService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, BulkStatusService bulkStatusService) {
public BulkFileProcessingService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, BulkProcessingService bulkStatusService) {
this.matrixRateExcelMapper = matrixRateExcelMapper;
this.containerRateExcelMapper = containerRateExcelMapper;
this.materialExcelMapper = materialExcelMapper;
@ -33,10 +34,11 @@ public class BulkFileProcessingService {
this.bulkStatusService = bulkStatusService;
}
public Integer processFile(BulkFileType type, BulkProcessingType processingType, MultipartFile file) {
public Integer processFile(BulkFileType type, MultipartFile file) {
//TODO: launch parallel task
String contentType = file.getContentType();
if (!"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet".equals(contentType) &&
!"application/vnd.ms-excel".equals(contentType)) {
@ -47,7 +49,6 @@ public class BulkFileProcessingService {
Workbook workbook = new XSSFWorkbook(in);
Sheet sheet = workbook.getSheet(BulkFileTypes.valueOf(type.name()).getSheetName());
//todo: if processing type = append than merge with current data, if full, delete old data
switch (type) {
case CONTAINER_RATE:
var containerRates = containerRateExcelMapper.extractSheet(sheet);
@ -70,13 +71,13 @@ public class BulkFileProcessingService {
}
} catch (Exception e) {
throw new BadRequestException("Unable to read excel sheet", e.getMessage(), e);
}
return null;
return 0;
}
public BulkStatus getStatus(Integer id) {
return bulkStatusService.getStatus(id);
public List<BulkStatusDTO> getStatus() {
return bulkStatusService.getStatus();
}
}

View file

@ -0,0 +1,30 @@
package de.avatic.lcc.service.bulk;
import de.avatic.lcc.dto.bulk.BulkProcessingType;
import de.avatic.lcc.dto.bulk.BulkStatusDTO;
import de.avatic.lcc.model.bulk.BulkProcess;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Queue;
@Service
public class BulkProcessingService {
int processCount = 0;
private Queue<BulkProcess> processes;
public Integer queueUpload(MultipartFile bulkRequest, BulkProcessingType processingType) {
return processCount++;
}
public List<BulkStatusDTO> getStatus() {
return null; //TODO implement me
}
}

View file

@ -1,12 +0,0 @@
package de.avatic.lcc.service.bulk;
import de.avatic.lcc.dto.bulk.BulkStatus;
import org.springframework.stereotype.Service;
@Service
public class BulkStatusService {
public BulkStatus getStatus(Integer id) {
return null; //TODO implement me
}
}

View file

@ -2,6 +2,7 @@ package de.avatic.lcc.service.bulk;
import de.avatic.lcc.dto.bulk.BulkFileType;
import de.avatic.lcc.model.bulk.*;
import de.avatic.lcc.model.bulk.header.*;
import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import de.avatic.lcc.service.excelMapper.*;

View file

@ -1,21 +1,17 @@
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 de.avatic.lcc.util.exception.internalerror.ExcelValidationError;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.springframework.stereotype.Service;
import java.util.EnumSet;
import java.util.function.Function;
@Service
public class ConstraintGenerator {
private static final int MAX_ROWS = 10000;
private static final int MAX_ROWS = 100000;
public void createBooleanConstraint(Sheet sheet, Integer columnIdx) {
@ -26,16 +22,11 @@ public class ConstraintGenerator {
createConstraint(sheet, columnIdx, EnumSet.allOf(values).stream().map(Enum::name).toArray(String[]::new));
}
public <T extends Enum<T>> void createEnumConstraint(Sheet sheet, Integer columnIdx, Class<T> values, Function<T, String> resolver) {
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, MAX_ROWS, columnIdx, columnIdx));
String errorMessage = values.length > 8 ? "Please check dropdown for allowed values." :
@ -118,4 +109,115 @@ public class ConstraintGenerator {
}
return result.toString();
}
public void validateDecimalConstraint(Row row, int columnIdx, double min, double max) {
CellType cellType = row.getCell(columnIdx).getCellType();
if(cellType == CellType.NUMERIC) {
double value = row.getCell(columnIdx).getNumericCellValue();
if(value >= min && value <= max) {
return;
}
}
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected numeric value within range: " + min + " - " + max);
}
public void validateBooleanConstraint(Row row, int columnIdx) {
CellType cellType = row.getCell(columnIdx).getCellType();
if (cellType == CellType.STRING) {
try {
//noinspection ResultOfMethodCallIgnored
Boolean.valueOf(row.getCell(columnIdx).getStringCellValue());
return;
} catch (Exception e) {
// cont
}
}
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected string value within: [true, false]");
}
public void validateLengthConstraint(Row row, int columnIdx, int min, int max) {
CellType cellType = row.getCell(columnIdx).getCellType();
if (cellType == CellType.STRING) {
String value = row.getCell(columnIdx).getStringCellValue();
if (value.length() >= min && value.length() <= max) {
return;
}
}
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected string with length within range: " + min + " - " + max);
}
public <T extends Enum<T>> void validateEnumConstraint(Row row, int columnIdx, Class<T> clazz) {
CellType cellType = row.getCell(columnIdx).getCellType();
if (cellType == CellType.STRING) {
String value = row.getCell(columnIdx).getStringCellValue();
if (EnumSet.allOf(clazz).stream().anyMatch(enumValue -> enumValue.name().equals(value))) {
return;
}
}
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected string value within: [" + String.join(", ", EnumSet.allOf(clazz).stream().map(Enum::name).toArray(String[]::new)) + "]");
}
public void validateNumericCell(Row row, int columnIdx) {
CellType cellType = row.getCell(columnIdx).getCellType();
if (cellType == CellType.NUMERIC) {
Double value = row.getCell(columnIdx).getNumericCellValue();
return;
}
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected numeric value");
}
public void validateStringCell(Row row, int columnIdx) {
CellType cellType = row.getCell(columnIdx).getCellType();
if (cellType == CellType.STRING) {
String value = row.getCell(columnIdx).getStringCellValue();
return;
}
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected numeric value");
}
public void validateIntegerConstraint(Row row, int columnIdx, int min, int max) {
CellType cellType = row.getCell(columnIdx).getCellType();
if(cellType == CellType.NUMERIC) {
double value = row.getCell(columnIdx).getNumericCellValue();
if(value % 1 == 0 && value >= min && value <= max) {
return;
}
}
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected numeric integer value within range: " + min + " - " + max);
}
}

View file

@ -1,7 +1,7 @@
package de.avatic.lcc.service.bulk.helper;
import de.avatic.lcc.model.bulk.HeaderProvider;
import de.avatic.lcc.model.bulk.NodeHeader;
import de.avatic.lcc.model.bulk.header.HeaderProvider;
import de.avatic.lcc.util.exception.internalerror.ExcelValidationError;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
@ -15,27 +15,26 @@ public class HeaderGenerator {
private static final int ADD_COLUMN_SIZE = (10*256);
public <H extends Enum<H> & HeaderProvider> boolean validateHeader(Sheet sheet, Class<H> headers) {
public <H extends Enum<H> & HeaderProvider> void validateHeader(Sheet sheet, Class<H> headers) {
Row row = sheet.getRow(0);
for(H header : EnumSet.allOf(headers)){
Cell cell = row.getCell(header.ordinal());
if(cell == null || !cell.getStringCellValue().equals(header.getHeader())){
return false;
throw new ExcelValidationError("Unable to validate header " + header.getHeader() + ": Header of column " + toExcelLetter(header.ordinal()) + " has to be " + header.getHeader());
}
}
return true;
}
public boolean validateHeader(Sheet sheet, String[] headers) {
public void validateHeader(Sheet sheet, String[] headers) {
Row row = sheet.getRow(0);
int idx = 0;
for(String header : headers){
Cell cell = row.getCell(idx++);
if(cell == null || !cell.getStringCellValue().equals(header)){
return false;
throw new ExcelValidationError("Unable to validate header " + header + ": Header of column " + toExcelLetter(idx) + " has to be " + header);
}
}
return true;
}
public <H extends Enum<H> & HeaderProvider> void generateHeader(Sheet worksheet, Class<H> headers, CellStyle style) {
@ -75,4 +74,15 @@ public class HeaderGenerator {
sheet.setColumnWidth(header.ordinal(),sheet.getColumnWidth(header.ordinal())+ADD_COLUMN_SIZE);
}
}
private String toExcelLetter(int columnIdx) {
StringBuilder result = new StringBuilder();
columnIdx++; // Convert from 0-based to 1-based for the algorithm
while (columnIdx > 0) {
columnIdx--; // Adjust for 1-based indexing
result.insert(0, (char) ('A' + columnIdx % 26));
columnIdx /= 26;
}
return result.toString();
}
}

View file

@ -0,0 +1,34 @@
package de.avatic.lcc.service.error;
import de.avatic.lcc.dto.error.ErrorLogDTO;
import de.avatic.lcc.dto.error.FrontendErrorDTO;
import de.avatic.lcc.repositories.error.SysErrorRepository;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import de.avatic.lcc.service.transformer.error.SysErrorMapper;
import jakarta.validation.constraints.Min;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class SysErrorService {
private final SysErrorRepository sysErrorRepository;
private final SysErrorMapper sysErrorMapper;
public SysErrorService(SysErrorRepository sysErrorRepository, SysErrorMapper sysErrorMapper) {
this.sysErrorRepository = sysErrorRepository;
this.sysErrorMapper = sysErrorMapper;
}
public void addErrors(List<FrontendErrorDTO> dto) {
sysErrorRepository.insert(dto.stream().map(sysErrorMapper::toSysErrorEntity).toList());
}
public SearchQueryResult<ErrorLogDTO> listErrors(Optional<String> filter, @Min(1) int page, @Min(1) int limit) {
return SearchQueryResult.map(sysErrorRepository.listErrors(filter, new SearchQueryPagination(page, limit)), sysErrorMapper::toSysErrorDto);
}
}

View file

@ -1,8 +1,8 @@
package de.avatic.lcc.service.excelMapper;
import de.avatic.lcc.dto.generic.TransportType;
import de.avatic.lcc.model.bulk.ContainerRateHeader;
import de.avatic.lcc.model.bulk.HiddenNodeHeader;
import de.avatic.lcc.model.bulk.header.ContainerRateHeader;
import de.avatic.lcc.model.bulk.header.HiddenNodeHeader;
import de.avatic.lcc.model.bulk.HiddenTableType;
import de.avatic.lcc.model.rates.ContainerRate;
import de.avatic.lcc.repositories.NodeRepository;
@ -67,18 +67,25 @@ public class ContainerRateExcelMapper {
}
public List<ContainerRate> extractSheet(Sheet sheet) {
if(!headerGenerator.validateHeader(sheet, ContainerRateHeader.class)) return null;
headerGenerator.validateHeader(sheet, ContainerRateHeader.class);
var rates = new ArrayList<ContainerRate>();
sheet.forEach(row -> rates.add(mapToEntity(row)));
sheet.forEach(row -> {
if(row.getRowNum() == 0) return;
rates.add(mapToEntity(row));
});
return rates;
}
private ContainerRate mapToEntity(Row row) {
ContainerRate entity = new ContainerRate();
validateConstraints(row);
entity.setFromNodeId(nodeRepository.getByExternalMappingId(row.getCell(ContainerRateHeader.FROM_NODE.ordinal()).getStringCellValue()).orElseThrow().getId());
entity.setFromNodeId(nodeRepository.getByExternalMappingId(row.getCell(ContainerRateHeader.TO_NODE.ordinal()).getStringCellValue()).orElseThrow().getId());
entity.setToNodeId(nodeRepository.getByExternalMappingId(row.getCell(ContainerRateHeader.TO_NODE.ordinal()).getStringCellValue()).orElseThrow().getId());
entity.setType(TransportType.valueOf(row.getCell(ContainerRateHeader.CONTAINER_RATE_TYPE.ordinal()).getStringCellValue()));
entity.setLeadTime(Double.valueOf(row.getCell(ContainerRateHeader.LEAD_TIME.ordinal()).getNumericCellValue()).intValue());
entity.setType(TransportType.valueOf(row.getCell(ContainerRateHeader.CONTAINER_RATE_TYPE.ordinal()).getStringCellValue()));
@ -88,4 +95,16 @@ public class ContainerRateExcelMapper {
return entity;
}
private void validateConstraints(Row row) {
constraintGenerator.validateStringCell(row, ContainerRateHeader.FROM_NODE.ordinal());
constraintGenerator.validateStringCell(row, ContainerRateHeader.TO_NODE.ordinal());
constraintGenerator.validateEnumConstraint(row, ContainerRateHeader.CONTAINER_RATE_TYPE.ordinal(), TransportType.class);
constraintGenerator.validateDecimalConstraint(row, ContainerRateHeader.RATE_FEU.ordinal(), 0.0, 1000000.0);
constraintGenerator.validateDecimalConstraint(row, ContainerRateHeader.RATE_TEU.ordinal(), 0.0, 1000000.0);
constraintGenerator.validateDecimalConstraint(row, ContainerRateHeader.RATE_HC.ordinal(), 0.0, 1000000.0);
constraintGenerator.validateIntegerConstraint(row, ContainerRateHeader.LEAD_TIME.ordinal(), 0, 365);
}
}

View file

@ -1,6 +1,6 @@
package de.avatic.lcc.service.excelMapper;
import de.avatic.lcc.model.bulk.HiddenCountryHeader;
import de.avatic.lcc.model.bulk.header.HiddenCountryHeader;
import de.avatic.lcc.model.country.Country;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;

View file

@ -1,6 +1,6 @@
package de.avatic.lcc.service.excelMapper;
import de.avatic.lcc.model.bulk.HiddenNodeHeader;
import de.avatic.lcc.model.bulk.header.HiddenNodeHeader;
import de.avatic.lcc.model.nodes.Node;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;

View file

@ -1,6 +1,8 @@
package de.avatic.lcc.service.excelMapper;
import de.avatic.lcc.model.bulk.MaterialHeader;
import de.avatic.lcc.model.bulk.BulkOperation;
import de.avatic.lcc.model.bulk.BulkOperationType;
import de.avatic.lcc.model.bulk.header.MaterialHeader;
import de.avatic.lcc.model.materials.Material;
import de.avatic.lcc.repositories.MaterialRepository;
import de.avatic.lcc.service.bulk.helper.ConstraintGenerator;
@ -33,6 +35,10 @@ public class MaterialExcelMapper {
}
private void mapToRow(Material material, Row row) {
row.createCell(MaterialHeader.OPERATION.ordinal()).setCellValue(BulkOperationType.UPDATE.name());
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());
@ -42,20 +48,30 @@ public class MaterialExcelMapper {
constraintGenerator.createLengthConstraint(sheet, MaterialHeader.PART_NUMBER.ordinal(), 0, 12);
constraintGenerator.createLengthConstraint(sheet, MaterialHeader.HS_CODE.ordinal(), 0, 11);
constraintGenerator.createLengthConstraint(sheet, MaterialHeader.DESCRIPTION.ordinal(), 1, 500);
constraintGenerator.createEnumConstraint(sheet, MaterialHeader.OPERATION.ordinal(), BulkOperationType.class);
}
public List<Material> extractSheet(Sheet sheet) {
if(!headerGenerator.validateHeader(sheet, MaterialHeader.class)) return null;
public List<BulkOperation<Material>> extractSheet(Sheet sheet) {
headerGenerator.validateHeader(sheet, MaterialHeader.class);
var materials = new ArrayList<Material>();
sheet.forEach(row -> materials.add(mapToEntity(row)));
var materials = new ArrayList<BulkOperation<Material>>();
sheet.forEach(row -> {
if(row.getRowNum() == 0) return;
materials.add(mapToEntity(row));
});
return materials;
}
private Material mapToEntity(Row row) {
private BulkOperation<Material> mapToEntity(Row row) {
Material entity = new Material();
constraintGenerator.validateLengthConstraint(row, MaterialHeader.PART_NUMBER.ordinal(), 0, 12);
constraintGenerator.validateLengthConstraint(row, MaterialHeader.HS_CODE.ordinal(), 0, 11);
constraintGenerator.validateLengthConstraint(row, MaterialHeader.DESCRIPTION.ordinal(), 1, 500);
constraintGenerator.validateEnumConstraint(row, MaterialHeader.OPERATION.ordinal(), BulkOperationType.class);
entity.setPartNumber(row.getCell(MaterialHeader.PART_NUMBER.ordinal()).getStringCellValue());
entity.setName(row.getCell(MaterialHeader.DESCRIPTION.ordinal()).getStringCellValue());
entity.setHsCode(row.getCell(MaterialHeader.HS_CODE.ordinal()).getStringCellValue());
@ -64,7 +80,7 @@ public class MaterialExcelMapper {
if(!validateHsCode(entity.getHsCode())) throw new IllegalArgumentException("Invalid HS Code");
return entity;
return new BulkOperation<>(entity,BulkOperationType.valueOf(row.getCell(MaterialHeader.OPERATION.ordinal()).getStringCellValue()));
}
private String normalizePartNumber(String partNumber) {

View file

@ -1,6 +1,8 @@
package de.avatic.lcc.service.excelMapper;
import de.avatic.lcc.model.bulk.*;
import de.avatic.lcc.model.bulk.header.HiddenCountryHeader;
import de.avatic.lcc.model.bulk.header.MatrixRateHeader;
import de.avatic.lcc.model.country.IsoCode;
import de.avatic.lcc.model.rates.MatrixRate;
import de.avatic.lcc.repositories.country.CountryRepository;
@ -39,16 +41,25 @@ public class MatrixRateExcelMapper {
}
public List<MatrixRate> extractSheet(Sheet sheet) {
if(!headerGenerator.validateHeader(sheet, MatrixRateHeader.class)) return null;
headerGenerator.validateHeader(sheet, MatrixRateHeader.class);
List<MatrixRate> rates = new ArrayList<>();
sheet.forEach(row -> rates.add(mapToEntity(row)));
sheet.forEach(row -> {
if(row.getRowNum() == 0) return;
rates.add(mapToEntity(row));
});
return rates;
}
private MatrixRate mapToEntity(Row row) {
MatrixRate entity = new MatrixRate();
constraintGenerator.validateStringCell(row, MatrixRateHeader.FROM_COUNTRY.ordinal());
constraintGenerator.validateStringCell(row, MatrixRateHeader.TO_COUNTRY.ordinal());
constraintGenerator.validateDecimalConstraint(row, MatrixRateHeader.RATE.ordinal(), 0.0, 1000000.0);
entity.setToCountry(countryRepository.getByIsoCode(IsoCode.valueOf(row.getCell(MatrixRateHeader.TO_COUNTRY.ordinal()).getStringCellValue())).orElseThrow().getId());
entity.setFromCountry(countryRepository.getByIsoCode(IsoCode.valueOf(row.getCell(MatrixRateHeader.FROM_COUNTRY.ordinal()).getStringCellValue())).orElseThrow().getId());
entity.setRate(BigDecimal.valueOf(row.getCell(MatrixRateHeader.RATE.ordinal()).getNumericCellValue()));

View file

@ -1,9 +1,9 @@
package de.avatic.lcc.service.excelMapper;
import de.avatic.lcc.excelmodel.ExcelNode;
import de.avatic.lcc.model.bulk.HiddenCountryHeader;
import de.avatic.lcc.model.bulk.HiddenTableType;
import de.avatic.lcc.model.bulk.NodeHeader;
import de.avatic.lcc.model.bulk.*;
import de.avatic.lcc.model.bulk.header.HiddenCountryHeader;
import de.avatic.lcc.model.bulk.header.NodeHeader;
import de.avatic.lcc.model.country.Country;
import de.avatic.lcc.model.country.IsoCode;
import de.avatic.lcc.model.nodes.Node;
@ -37,26 +37,26 @@ public class NodeExcelMapper {
this.constraintGenerator = constraintGenerator;
}
public void fillSheet(Sheet sheet, CellStyle headerStyle)
{
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)));
nodeRepository.listAllNodes(false).forEach(node -> mapToRow(node, sheet.createRow(sheet.getLastRowNum() + 1)));
headerGenerator.fixWidth(sheet, NodeHeader.class);
}
private void mapToRow(Node node, Row row) {
row.createCell(NodeHeader.OPERATION.ordinal()).setCellValue(BulkOperationType.UPDATE.name());
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_SOURCE.ordinal()).setCellValue(node.getSource() ? "true":"false");
row.createCell(NodeHeader.IS_INTERMEDIATE.ordinal()).setCellValue(node.getIntermediate()? "true":"false");
row.createCell(NodeHeader.IS_DESTINATION.ordinal()).setCellValue(node.getDestination()? "true":"false");
row.createCell(NodeHeader.IS_SOURCE.ordinal()).setCellValue(node.getSource() ? "true" : "false");
row.createCell(NodeHeader.IS_INTERMEDIATE.ordinal()).setCellValue(node.getIntermediate() ? "true" : "false");
row.createCell(NodeHeader.IS_DESTINATION.ordinal()).setCellValue(node.getDestination() ? "true" : "false");
row.createCell(NodeHeader.OUTBOUND_COUNTRIES.ordinal()).setCellValue(mapOutboundCountriesToCell(node.getOutboundCountries()));
row.createCell(NodeHeader.PREDECESSOR_NODES.ordinal()).setCellValue(mapChains(node.getNodePredecessors()));
row.createCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).setCellValue(node.getPredecessorRequired()? "true":"false");
row.createCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).setCellValue(node.getPredecessorRequired() ? "true" : "false");
}
private String mapChains(List<Map<Integer, Integer>> chains) {
@ -86,42 +86,71 @@ public class NodeExcelMapper {
constraintGenerator.createLengthConstraint(sheet, NodeHeader.ADDRESS.ordinal(), 1, 500);
constraintGenerator.createLengthConstraint(sheet, NodeHeader.NAME.ordinal(), 1, 255);
constraintGenerator.createEnumConstraint(sheet, NodeHeader.OPERATION.ordinal(), BulkOperationType.class);
}
public List<ExcelNode> extractSheet(Sheet sheet) {
if(!headerGenerator.validateHeader(sheet, NodeHeader.class)) return null;
public List<BulkOperation<ExcelNode>> extractSheet(Sheet sheet) {
headerGenerator.validateHeader(sheet, NodeHeader.class);
var nodes = new ArrayList<ExcelNode>();
sheet.forEach(row -> nodes.add(mapToEntity(row)));
var nodes = new ArrayList<BulkOperation<ExcelNode>>();
sheet.forEach(row -> {
if (row.getRowNum() == 0) return;
nodes.add(mapToEntity(row));
});
return nodes;
}
private ExcelNode mapToEntity(Row row) {
private BulkOperation<ExcelNode> mapToEntity(Row row) {
ExcelNode entity = new ExcelNode();
validateConstraints(row);
entity.setExternalMappingId(row.getCell(NodeHeader.MAPPING_ID.ordinal()).getStringCellValue());
entity.setName(row.getCell(NodeHeader.NAME.ordinal()).getStringCellValue());
entity.setAddress(row.getCell(NodeHeader.ADDRESS.ordinal()).getStringCellValue());
entity.setCountryId(IsoCode.valueOf(row.getCell(NodeHeader.COUNTRY.ordinal()).getStringCellValue()));
entity.setGeoLat(BigDecimal.valueOf(row.getCell(NodeHeader.GEO_LATITUDE.ordinal()).getNumericCellValue()));
entity.setGeoLng(BigDecimal.valueOf(row.getCell(NodeHeader.GEO_LONGITUDE.ordinal()).getNumericCellValue()));
entity.setSource(row.getCell(NodeHeader.IS_SOURCE.ordinal()).getBooleanCellValue());
entity.setIntermediate(row.getCell(NodeHeader.IS_INTERMEDIATE.ordinal()).getBooleanCellValue());
entity.setDestination(row.getCell(NodeHeader.IS_DESTINATION.ordinal()).getBooleanCellValue());
entity.setPredecessorRequired(row.getCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).getBooleanCellValue());
entity.setSource(Boolean.valueOf(row.getCell(NodeHeader.IS_SOURCE.ordinal()).getStringCellValue()));
entity.setIntermediate(Boolean.valueOf(row.getCell(NodeHeader.IS_INTERMEDIATE.ordinal()).getStringCellValue()));
entity.setDestination(Boolean.valueOf(row.getCell(NodeHeader.IS_DESTINATION.ordinal()).getStringCellValue()));
entity.setPredecessorRequired(Boolean.valueOf(row.getCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).getStringCellValue()));
entity.setNodePredecessors(mapChainsFromCell(row.getCell(NodeHeader.PREDECESSOR_NODES.ordinal()).getStringCellValue()));
entity.setOutboundCountries(mapOutboundCountriesFromCell(row.getCell(NodeHeader.OUTBOUND_COUNTRIES.ordinal()).getStringCellValue()));
return entity;
return new BulkOperation<>(entity, BulkOperationType.valueOf(row.getCell(NodeHeader.OPERATION.ordinal()).getStringCellValue()));
}
private void validateConstraints(Row row) {
constraintGenerator.validateStringCell(row, NodeHeader.MAPPING_ID.ordinal());
constraintGenerator.validateStringCell(row, NodeHeader.COUNTRY.ordinal());
constraintGenerator.validateStringCell(row, NodeHeader.OUTBOUND_COUNTRIES.ordinal());
constraintGenerator.validateStringCell(row, NodeHeader.PREDECESSOR_NODES.ordinal());
constraintGenerator.validateStringCell(row, NodeHeader.NAME.ordinal());
constraintGenerator.validateStringCell(row, NodeHeader.ADDRESS.ordinal());
constraintGenerator.validateDecimalConstraint(row, NodeHeader.GEO_LATITUDE.ordinal(), -90.0, 90.0);
constraintGenerator.validateDecimalConstraint(row, NodeHeader.GEO_LONGITUDE.ordinal(), -180.0, 180.0);
constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_SOURCE.ordinal());
constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_INTERMEDIATE.ordinal());
constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_DESTINATION.ordinal());
constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal());
constraintGenerator.validateLengthConstraint(row, NodeHeader.ADDRESS.ordinal(), 1, 500);
constraintGenerator.validateLengthConstraint(row, NodeHeader.NAME.ordinal(), 1, 255);
constraintGenerator.validateEnumConstraint(row, NodeHeader.OPERATION.ordinal(), BulkOperationType.class);
}
private List<String> mapOutboundCountriesFromCell(String outboundCountryIds) {
if(outboundCountryIds == null || outboundCountryIds.isBlank()) return Collections.emptyList();
if (outboundCountryIds == null || outboundCountryIds.isBlank()) return Collections.emptyList();
return Arrays.stream(outboundCountryIds.split(",")).map(String::trim).toList();
}
private List<List<String>> mapChainsFromCell(String cell) {
if(cell == null || cell.isBlank()) return Collections.emptyList();
if (cell == null || cell.isBlank()) return Collections.emptyList();
return Arrays.stream(cell.split(";")).map(String::trim).map(this::mapChainFromCell).toList();
}

View file

@ -2,9 +2,9 @@ package de.avatic.lcc.service.excelMapper;
import de.avatic.lcc.excelmodel.ExcelPackaging;
import de.avatic.lcc.model.bulk.HiddenNodeHeader;
import de.avatic.lcc.model.bulk.HiddenTableType;
import de.avatic.lcc.model.bulk.PackagingHeader;
import de.avatic.lcc.model.bulk.*;
import de.avatic.lcc.model.bulk.header.HiddenNodeHeader;
import de.avatic.lcc.model.bulk.header.PackagingHeader;
import de.avatic.lcc.model.packaging.Packaging;
import de.avatic.lcc.model.packaging.PackagingDimension;
import de.avatic.lcc.model.properties.PropertyType;
@ -61,6 +61,8 @@ public class PackagingExcelMapper {
Optional<PackagingDimension> shu = packagingDimensionRepository.getById(packaging.getShuId());
Optional<PackagingDimension> hu = packagingDimensionRepository.getById(packaging.getShuId());
row.createCell(PackagingHeader.OPERATION.ordinal()).setCellValue(BulkOperationType.UPDATE.name());
row.createCell(PackagingHeader.PART_NUMBER.ordinal()).setCellValue(materialRepository.getByIdIncludeDeprecated(packaging.getMaterialId()).orElseThrow().getPartNumber());
row.createCell(PackagingHeader.SUPPLIER.ordinal()).setCellValue(nodeRepository.getById(packaging.getSupplierId()).orElseThrow().getExternalMappingId());
@ -104,9 +106,9 @@ public class PackagingExcelMapper {
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())).getDisplayedName());
cell.setCellValue(((DimensionUnit) resolver.apply(dimension.get())).name());
if (resolver.apply(dimension.get()) instanceof WeightUnit)
cell.setCellValue(((WeightUnit) resolver.apply(dimension.get())).getDisplayedName());
cell.setCellValue(((WeightUnit) resolver.apply(dimension.get())).name());
} else cell.setBlank();
}
@ -121,6 +123,8 @@ public class PackagingExcelMapper {
constraintGenerator.createEnumConstraint(sheet, PackagingHeader.HU_DIMENSION_UNIT.ordinal(), DimensionUnit.class);
constraintGenerator.createEnumConstraint(sheet, PackagingHeader.HU_WEIGHT_UNIT.ordinal(), WeightUnit.class);
constraintGenerator.createEnumConstraint(sheet, PackagingHeader.OPERATION.ordinal(), BulkOperationType.class);
//TODO: check hu dimensions...
@ -129,10 +133,13 @@ public class PackagingExcelMapper {
}
public List<ExcelPackaging> extractSheet(Sheet sheet) {
if (!headerGenerator.validateHeader(sheet, PackagingHeader.class)) return null;
headerGenerator.validateHeader(sheet, PackagingHeader.class);
var packaging = new ArrayList<ExcelPackaging>();
sheet.forEach(row -> packaging.add(mapToEntity(row)));
sheet.forEach(row -> {
if(row.getRowNum() == 0) return;
packaging.add(mapToEntity(row));
});
return packaging;
}
@ -140,6 +147,8 @@ public class PackagingExcelMapper {
private ExcelPackaging mapToEntity(Row row) {
ExcelPackaging entity = new ExcelPackaging();
validateConstraints(row);
entity.setSupplierMappingId(row.getCell(PackagingHeader.SUPPLIER.ordinal()).getStringCellValue());
entity.setPartNumber(row.getCell(PackagingHeader.PART_NUMBER.ordinal()).getStringCellValue());
@ -162,4 +171,27 @@ public class PackagingExcelMapper {
return entity;
}
private void validateConstraints(Row row) {
constraintGenerator.validateStringCell(row, PackagingHeader.SUPPLIER.ordinal());
constraintGenerator.validateStringCell(row, PackagingHeader.PART_NUMBER.ordinal());
constraintGenerator.validateNumericCell(row, PackagingHeader.HU_HEIGHT.ordinal());
constraintGenerator.validateNumericCell(row, PackagingHeader.HU_WIDTH.ordinal());
constraintGenerator.validateNumericCell(row, PackagingHeader.HU_LENGTH.ordinal());
constraintGenerator.validateNumericCell(row, PackagingHeader.HU_WEIGHT.ordinal());
constraintGenerator.validateNumericCell(row, PackagingHeader.SHU_HEIGHT.ordinal());
constraintGenerator.validateNumericCell(row, PackagingHeader.SHU_WIDTH.ordinal());
constraintGenerator.validateNumericCell(row, PackagingHeader.SHU_LENGTH.ordinal());
constraintGenerator.validateNumericCell(row, PackagingHeader.SHU_WEIGHT.ordinal());
constraintGenerator.validateLengthConstraint(row, PackagingHeader.PART_NUMBER.ordinal(), 0, 12);
constraintGenerator.validateEnumConstraint(row, PackagingHeader.SHU_DIMENSION_UNIT.ordinal(), DimensionUnit.class);
constraintGenerator.validateEnumConstraint(row, PackagingHeader.SHU_WEIGHT_UNIT.ordinal(), WeightUnit.class);
constraintGenerator.validateEnumConstraint(row, PackagingHeader.HU_DIMENSION_UNIT.ordinal(), DimensionUnit.class);
constraintGenerator.validateEnumConstraint(row, PackagingHeader.HU_WEIGHT_UNIT.ordinal(), WeightUnit.class);
constraintGenerator.validateEnumConstraint(row, PackagingHeader.OPERATION.ordinal(), BulkOperationType.class);
}
}

View file

@ -0,0 +1,121 @@
package de.avatic.lcc.service.transformer.error;
import de.avatic.lcc.dto.error.ErrorLogDTO;
import de.avatic.lcc.dto.error.ErrorLogTraceItemDto;
import de.avatic.lcc.dto.error.FrontendErrorDTO;
import de.avatic.lcc.model.error.SysError;
import de.avatic.lcc.model.error.SysErrorTraceItem;
import de.avatic.lcc.model.error.SysErrorType;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class SysErrorMapper {
private static final String TRACE_REGEX = "at\\s+(?:async\\s+)?(?:(.+?)\\s+)?\\(([^?]+(?:\\?[^:]*)?):(\\d+):\\d+\\)";
private static final Pattern TRACE_REGEX_PATTERN = Pattern.compile(TRACE_REGEX);
public SysError toSysErrorEntity(FrontendErrorDTO frontendErrorDTO) {
int userId = 1; //TODO use actual user
SysError entity = new SysError();
entity.setCode(frontendErrorDTO.getError().getCode());
entity.setMessage(frontendErrorDTO.getError().getMessage());
entity.setTitle(frontendErrorDTO.getError().getTitle());
entity.setPinia(frontendErrorDTO.getState());
entity.setUserId(userId);
var traceItems = frontendErrorDTO.getError().getTrace();
if (traceItems == null) {
entity.setType(SysErrorType.FRONTEND);
var traceCombined = frontendErrorDTO.getError().getTraceCombined();
if (null != traceCombined) {
entity.setTrace(toSysErrorTraceItem(frontendErrorDTO.getError().getTraceCombined()));
}
} else {
entity.setType(SysErrorType.BACKEND);
entity.setTrace(toSysErrorTraceItem(frontendErrorDTO.getError().getTrace()));
}
return entity;
}
private List<SysErrorTraceItem> toSysErrorTraceItem(String traceCombined) {
List<SysErrorTraceItem> items = new ArrayList<>();
var lines = traceCombined.split("\n");
for (var line : lines) {
Matcher matcher = TRACE_REGEX_PATTERN.matcher(line);
while (matcher.find()) {
SysErrorTraceItem item = new SysErrorTraceItem();
item.setFile(matcher.group(2));
item.setLine(Integer.parseInt(matcher.group(3)));
item.setMethod(matcher.group(1));
item.setFullPath("at " + matcher.group(1));
items.add(item);
}
}
return items;
}
private List<SysErrorTraceItem> toSysErrorTraceItem(List<StackTraceElement> trace) {
List<SysErrorTraceItem> items = new ArrayList<>();
for(var traceElement : trace) {
SysErrorTraceItem item = new SysErrorTraceItem();
item.setFile(traceElement.getFileName());
item.setLine(traceElement.getLineNumber());
item.setMethod(traceElement.getMethodName());
item.setFullPath("at " + traceElement.getClassName() + "." + traceElement.getMethodName());
items.add(item);
}
return items;
}
public ErrorLogDTO toSysErrorDto(SysError sysError) {
var dto = new ErrorLogDTO();
dto.setUserId(sysError.getUserId());
dto.setCode(sysError.getCode());
dto.setMessage(sysError.getMessage());
dto.setTitle(sysError.getTitle());
dto.setPinia(sysError.getPinia());
dto.setBulkOperationId(sysError.getBulkOperationId());
dto.setCalculationJobId(sysError.getCalculationJobId());
dto.setType(sysError.getType().name());
dto.setTrace(sysError.getTrace().stream().map(this::toSysErrorTraceItemDto).toList());
dto.setCreatedAt(sysError.getCreatedAt());
return dto;
}
private ErrorLogTraceItemDto toSysErrorTraceItemDto(SysErrorTraceItem sysErrorTraceItem) {
var dto = new ErrorLogTraceItemDto();
dto.setFullPath(sysErrorTraceItem.getFullPath());
dto.setMethod(sysErrorTraceItem.getMethod());
dto.setLine(sysErrorTraceItem.getLine());
dto.setFile(sysErrorTraceItem.getFile());
return dto;
}
}

View file

@ -12,6 +12,12 @@ public class InternalErrorException extends RuntimeException{
this.message = message;
}
public InternalErrorException(String title, String message) {
super(message);
this.title = title;
this.message = message;
}
public InternalErrorException(String message, Exception trace) {
super(message, trace);
this.title = "Internal Error";

View file

@ -0,0 +1,11 @@
package de.avatic.lcc.util.exception.internalerror;
import de.avatic.lcc.util.exception.base.InternalErrorException;
public class ExcelValidationError extends InternalErrorException {
public ExcelValidationError(String message) {
super("Excel validation failed.", message);
}
}

View file

@ -5,7 +5,7 @@ import de.avatic.lcc.util.exception.base.InternalErrorException;
public class PremiseValidationError extends InternalErrorException {
public PremiseValidationError(String message) {
super(message);
super("Calculation data validation failed.", message);
}

View file

@ -11,4 +11,6 @@ lcc.allowed_cors=${ALLOWED_CORS_DOMAIN}
azure.maps.subscription.key=${AZURE_MAPS_SUBSCRIPTION_KEY}
azure.maps.client.id=your-app-registration-client-id
azure.maps.resource.id=/subscriptions/sub-id/resourceGroups/rg-name/providers/Microsoft.Maps/accounts/account-name
spring.servlet.multipart.max-file-size=30MB
spring.servlet.multipart.max-request-size=50MB

View file

@ -45,11 +45,11 @@ 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)',
`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)',
`name` VARCHAR(255) NOT NULL,
`is_deprecated` BOOLEAN NOT NULL DEFAULT FALSE,
`is_deprecated` BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_country_iso_code` (`iso_code`),
CONSTRAINT `chk_country_region_code`
@ -403,7 +403,7 @@ CREATE TABLE IF NOT EXISTS premise_route_node
user_node_id INT DEFAULT NULL,
name VARCHAR(255) NOT NULL,
address VARCHAR(500),
external_mapping_id VARCHAR(32) NOT NULL,
external_mapping_id VARCHAR(32) NOT NULL,
country_id INT NOT NULL,
is_destination BOOLEAN DEFAULT FALSE,
is_intermediate BOOLEAN DEFAULT FALSE,
@ -466,7 +466,7 @@ CREATE TABLE IF NOT EXISTS calculation_job
property_set_id INT NOT NULL,
job_state CHAR(10) NOT NULL CHECK (job_state IN
('CREATED', 'SCHEDULED', 'VALID', 'INVALID', 'EXCEPTION')),
error_id INT DEFAULT NULL,
error_id INT DEFAULT NULL,
user_id INT NOT NULL,
FOREIGN KEY (premise_id) REFERENCES premise (id),
FOREIGN KEY (validity_period_id) REFERENCES validity_period (id),
@ -565,3 +565,52 @@ CREATE TABLE IF NOT EXISTS calculation_job_route_section
);
CREATE TABLE IF NOT EXISTS bulk_operation
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
bulk_file_type CHAR(32) NOT NULL,
state CHAR(10) NOT NULL,
file LONGBLOB NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES sys_user (id),
CONSTRAINT chk_bulk_file_type CHECK (bulk_file_type IN
('CONTAINER_RATE', 'COUNTRY_MATRIX', 'MATERIAL', 'PACKAGING', 'NODE')),
CONSTRAINT chk_bulk_operation_state CHECK (state IN ('SCHEDULED', 'PROCESSING', 'COMPLETED', 'EXCEPTION'))
);
CREATE TABLE IF NOT EXISTS sys_error
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT DEFAULT NULL,
title VARCHAR(255) NOT NULL,
code VARCHAR(255) NOT NULL,
message VARCHAR(512) NOT NULL,
pinia TEXT,
calculation_job_id INT DEFAULT NULL,
bulk_operation_id INT DEFAULT NULL,
type CHAR(16) NOT NULL DEFAULT 'BACKEND',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES sys_user (id),
FOREIGN KEY (calculation_job_id) REFERENCES calculation_job (id),
FOREIGN KEY (bulk_operation_id) REFERENCES bulk_operation (id),
CONSTRAINT chk_error_type CHECK (type IN ('BACKEND', 'FRONTEND', 'BULK', 'CALCULATION')),
INDEX idx_user_id (user_id),
INDEX idx_calculation_job_id (calculation_job_id)
);
CREATE TABLE IF NOT EXISTS sys_error_trace_item
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
error_id INT NOT NULL,
line INT UNSIGNED NOT NULL,
file VARCHAR(255) NOT NULL,
method VARCHAR(255) NOT NULL,
fullPath VARCHAR(512) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (error_id) REFERENCES sys_error (id)
);