Initial commit

This commit is contained in:
2026-03-25 23:05:38 +01:00
commit 8a8db1e8a1
33 changed files with 2821 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
package de.gecheckt.asv.domain.model;
import java.util.Optional;
/**
* Represents a field in a segment of a message from an input file.
* A field has a position, a raw value and an optional name.
* This class represents parsed content from an input file, not validation rules.
*
* @param fieldPosition the position of the field (must be positive)
* @param rawValue the raw value of the field (must not be null)
* @param fieldName the name of the field (may be null)
*/
public record Field(int fieldPosition, String rawValue, String fieldName) {
/**
* Constructs a Field with the specified position and raw value.
*
* @param fieldPosition the position of the field (must be positive)
* @param rawValue the raw value of the field (must not be null)
* @throws IllegalArgumentException if fieldPosition is not positive or rawValue is null
*/
public Field(int fieldPosition, String rawValue) {
this(fieldPosition, rawValue, null);
}
/**
* Constructs a Field with the specified position, raw value and name.
*
* @param fieldPosition the position of the field (must be positive)
* @param rawValue the raw value of the field (must not be null)
* @param fieldName the name of the field (may be null)
* @throws IllegalArgumentException if fieldPosition is not positive or rawValue is null
*/
public Field {
if (fieldPosition <= 0) {
throw new IllegalArgumentException("Field position must be positive");
}
if (rawValue == null) {
throw new IllegalArgumentException("Raw value must not be null");
}
}
/**
* Returns the name of the field, if present.
*
* @return an Optional containing the field name, or empty if no name is set
*/
public Optional<String> getFieldName() {
return Optional.ofNullable(fieldName);
}
}

View File

@@ -0,0 +1,60 @@
package de.gecheckt.asv.domain.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Represents an input file containing messages.
* An input file has a source file name and contains multiple messages.
* This class represents parsed content from an input file, not validation rules.
*
* @param sourceFileName the name of the source file (must not be null or empty)
* @param messages the list of messages (must not be null)
*/
public record InputFile(String sourceFileName, List<Message> messages) {
/**
* Constructs an InputFile with the specified source file name.
*
* @param sourceFileName the name of the source file (must not be null or empty)
* @throws IllegalArgumentException if sourceFileName is null or empty
*/
public InputFile(String sourceFileName) {
this(sourceFileName, new ArrayList<>());
}
/**
* Constructs an InputFile with the specified source file name and messages.
*
* @param sourceFileName the name of the source file (must not be null or empty)
* @param messages the list of messages (must not be null)
* @throws IllegalArgumentException if sourceFileName is null or empty, or messages is null
*/
public InputFile {
if (sourceFileName == null || sourceFileName.isEmpty()) {
throw new IllegalArgumentException("Source file name must not be null or empty");
}
if (messages == null) {
throw new IllegalArgumentException("Messages must not be null");
}
}
/**
* Returns an unmodifiable list of all messages in the input file.
*
* @return an unmodifiable list of all messages
*/
public List<Message> getMessages() {
return Collections.unmodifiableList(messages);
}
/**
* Returns the number of messages in this input file.
*
* @return the number of messages
*/
public int getMessageCount() {
return messages.size();
}
}

View File

@@ -0,0 +1,112 @@
package de.gecheckt.asv.domain.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Represents a message in an input file.
* A message has a position and contains multiple segments.
* This class represents parsed content from an input file, not validation rules.
*
* @param messagePosition the position of the message (must be positive)
* @param segments the list of segments (must not be null)
*/
public record Message(int messagePosition, List<Segment> segments) {
/**
* Constructs a Message with the specified position.
*
* @param messagePosition the position of the message (must be positive)
* @throws IllegalArgumentException if messagePosition is not positive
*/
public Message(int messagePosition) {
this(messagePosition, new ArrayList<>());
}
/**
* Constructs a Message with the specified position and segments.
*
* @param messagePosition the position of the message (must be positive)
* @param segments the list of segments (must not be null)
* @throws IllegalArgumentException if messagePosition is not positive, or segments is null
*/
public Message {
if (messagePosition <= 0) {
throw new IllegalArgumentException("Message position must be positive");
}
if (segments == null) {
throw new IllegalArgumentException("Segments must not be null");
}
}
/**
* Returns an unmodifiable list of all segments in the message.
*
* @return an unmodifiable list of all segments
*/
public List<Segment> getSegments() {
return Collections.unmodifiableList(segments);
}
/**
* Checks if a segment with the specified name exists in this message.
*
* @param segmentName the name of the segment to check for (must not be null)
* @return true if a segment with the specified name exists, false otherwise
* @throws IllegalArgumentException if segmentName is null
*/
public boolean hasSegment(String segmentName) {
if (segmentName == null) {
throw new IllegalArgumentException("Segment name must not be null");
}
return segments.stream()
.anyMatch(segment -> segmentName.equals(segment.segmentName()));
}
/**
* Returns the number of segments in this message.
*
* @return the number of segments
*/
public int getSegmentCount() {
return segments.size();
}
/**
* Returns a list of segments with the specified name.
*
* @param segmentName the name of the segments to retrieve (must not be null)
* @return a list of segments with the specified name
* @throws IllegalArgumentException if segmentName is null
*/
public List<Segment> getSegments(String segmentName) {
if (segmentName == null) {
throw new IllegalArgumentException("Segment name must not be null");
}
return segments.stream()
.filter(segment -> segmentName.equals(segment.segmentName()))
.collect(Collectors.toList());
}
/**
* Returns the first segment with the specified name, if it exists.
*
* @param segmentName the name of the segment to retrieve (must not be null)
* @return an Optional containing the first segment with the specified name, or empty if no such segment exists
* @throws IllegalArgumentException if segmentName is null
*/
public Optional<Segment> getFirstSegment(String segmentName) {
if (segmentName == null) {
throw new IllegalArgumentException("Segment name must not be null");
}
return segments.stream()
.filter(segment -> segmentName.equals(segment.segmentName()))
.findFirst();
}
}

View File

@@ -0,0 +1,91 @@
package de.gecheckt.asv.domain.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Represents a segment in a message from an input file.
* A segment has a name, a position and contains multiple fields.
* This class represents parsed content from an input file, not validation rules.
*
* @param segmentName the name of the segment (must not be null or empty)
* @param segmentPosition the position of the segment (must be positive)
* @param fields the list of fields (must not be null)
*/
public record Segment(String segmentName, int segmentPosition, List<Field> fields) {
/**
* Constructs a Segment with the specified name and position.
*
* @param segmentName the name of the segment (must not be null or empty)
* @param segmentPosition the position of the segment (must be positive)
* @throws IllegalArgumentException if segmentName is null or empty, or segmentPosition is not positive
*/
public Segment(String segmentName, int segmentPosition) {
this(segmentName, segmentPosition, new ArrayList<>());
}
/**
* Constructs a Segment with the specified name, position and fields.
*
* @param segmentName the name of the segment (must not be null or empty)
* @param segmentPosition the position of the segment (must be positive)
* @param fields the list of fields (must not be null)
* @throws IllegalArgumentException if segmentName is null or empty, or segmentPosition is not positive, or fields is null
*/
public Segment {
if (segmentName == null || segmentName.isEmpty()) {
throw new IllegalArgumentException("Segment name must not be null or empty");
}
if (segmentPosition <= 0) {
throw new IllegalArgumentException("Segment position must be positive");
}
if (fields == null) {
throw new IllegalArgumentException("Fields must not be null");
}
}
/**
* Returns an unmodifiable list of all fields in the segment.
*
* @return an unmodifiable list of all fields
*/
public List<Field> getFields() {
return Collections.unmodifiableList(fields);
}
/**
* Checks if a field exists at the specified position.
*
* @param fieldPosition the position to check for a field
* @return true if a field exists at the specified position, false otherwise
*/
public boolean hasFieldAt(int fieldPosition) {
return fields.stream()
.anyMatch(field -> field.fieldPosition() == fieldPosition);
}
/**
* Returns the number of fields in this segment.
*
* @return the number of fields
*/
public int getFieldCount() {
return fields.size();
}
/**
* Returns the field at the specified position, if it exists.
*
* @param fieldPosition the position of the field to retrieve
* @return an Optional containing the field at the specified position, or empty if no such field exists
*/
public Optional<Field> getField(int fieldPosition) {
return fields.stream()
.filter(field -> field.fieldPosition() == fieldPosition)
.findFirst();
}
}

View File

@@ -0,0 +1,64 @@
package de.gecheckt.asv.parser;
import de.gecheckt.asv.domain.model.Field;
import de.gecheckt.asv.domain.model.InputFile;
import de.gecheckt.asv.domain.model.Message;
import de.gecheckt.asv.domain.model.Segment;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
/**
* Default implementation of InputFileParser.
*/
public class DefaultInputFileParser implements InputFileParser {
private final SegmentLineTokenizer tokenizer;
/**
* Constructs a DefaultInputFileParser with the specified tokenizer.
*
* @param tokenizer the tokenizer to use for parsing segment lines
*/
public DefaultInputFileParser(SegmentLineTokenizer tokenizer) {
this.tokenizer = tokenizer;
}
@Override
public InputFile parse(String fileName, String fileContent) throws IOException {
if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("File name must not be null or empty");
}
if (fileContent == null) {
throw new IllegalArgumentException("File content must not be null");
}
try (BufferedReader reader = new BufferedReader(new StringReader(fileContent))) {
List<Segment> segments = new ArrayList<>();
String line;
int segmentPosition = 1;
while ((line = reader.readLine()) != null) {
// Ignore empty lines
if (!line.trim().isEmpty()) {
String segmentName = tokenizer.extractSegmentName(line);
List<Field> fields = tokenizer.tokenizeFields(line);
Segment segment = new Segment(segmentName, segmentPosition, fields);
segments.add(segment);
segmentPosition++;
}
}
// For this simplified version, we assume exactly one message per file
Message message = new Message(1, segments);
List<Message> messages = new ArrayList<>();
messages.add(message);
return new InputFile(fileName, messages);
} catch (Exception e) {
throw new IOException("Error parsing file: " + fileName, e);
}
}
}

View File

@@ -0,0 +1,48 @@
package de.gecheckt.asv.parser;
import de.gecheckt.asv.domain.model.Field;
import java.util.ArrayList;
import java.util.List;
/**
* Default implementation of SegmentLineTokenizer that uses '+' as field separator
* and assumes the first token is the segment name.
*/
public class DefaultSegmentLineTokenizer implements SegmentLineTokenizer {
private static final String FIELD_SEPARATOR = "+";
@Override
public String extractSegmentName(String segmentLine) {
if (segmentLine == null || segmentLine.isEmpty()) {
return "";
}
int separatorIndex = segmentLine.indexOf(FIELD_SEPARATOR);
if (separatorIndex == -1) {
// If no separator found, the entire line is the segment name
return segmentLine;
}
return segmentLine.substring(0, separatorIndex);
}
@Override
public List<Field> tokenizeFields(String segmentLine) {
List<Field> fields = new ArrayList<>();
if (segmentLine == null || segmentLine.isEmpty()) {
return fields;
}
String[] tokens = segmentLine.split(java.util.regex.Pattern.quote(FIELD_SEPARATOR));
// Start from index 1 since index 0 is the segment name
for (int i = 1; i < tokens.length; i++) {
// Field positions are 1-based
fields.add(new Field(i, tokens[i]));
}
return fields;
}
}

View File

@@ -0,0 +1,20 @@
package de.gecheckt.asv.parser;
import de.gecheckt.asv.domain.model.InputFile;
import java.io.IOException;
/**
* Interface for parsing input files into the domain model.
*/
public interface InputFileParser {
/**
* Parses the content of a file into an InputFile domain object.
*
* @param fileName the name of the file to parse
* @param fileContent the content of the file to parse
* @return the parsed InputFile domain object
* @throws IOException if there is an error reading or parsing the file
*/
InputFile parse(String fileName, String fileContent) throws IOException;
}

View File

@@ -0,0 +1,26 @@
package de.gecheckt.asv.parser;
import de.gecheckt.asv.domain.model.Field;
import java.util.List;
/**
* Interface for splitting a segment line into its components.
*/
public interface SegmentLineTokenizer {
/**
* Extracts the segment name from a segment line.
*
* @param segmentLine the line to extract the segment name from
* @return the segment name
*/
String extractSegmentName(String segmentLine);
/**
* Splits a segment line into fields.
*
* @param segmentLine the line to split into fields
* @return the list of fields
*/
List<Field> tokenizeFields(String segmentLine);
}

View File

@@ -0,0 +1,56 @@
package de.gecheckt.asv.validation;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import de.gecheckt.asv.domain.model.InputFile;
import de.gecheckt.asv.validation.field.FieldValidator;
import de.gecheckt.asv.validation.model.ValidationResult;
import de.gecheckt.asv.validation.structure.StructureValidator;
/**
* Default implementation of InputFileValidator that orchestrates multiple specialized validators.
*
* This orchestrator executes validators in a predefined order:
* 1. StructureValidator - validates structural integrity
* 2. FieldValidator - validates field-specific rules
*
* Additional validators can be added in the future by extending this class or modifying the validation sequence.
*/
public class DefaultInputFileValidator implements InputFileValidator {
private final StructureValidator structureValidator;
private final FieldValidator fieldValidator;
/**
* Constructs a DefaultInputFileValidator with the required validators.
*
* @param structureValidator the structure validator to use (must not be null)
* @param fieldValidator the field validator to use (must not be null)
*/
public DefaultInputFileValidator(StructureValidator structureValidator, FieldValidator fieldValidator) {
this.structureValidator = Objects.requireNonNull(structureValidator, "structureValidator must not be null");
this.fieldValidator = Objects.requireNonNull(fieldValidator, "fieldValidator must not be null");
}
@Override
public ValidationResult validate(InputFile inputFile) {
if (inputFile == null) {
throw new IllegalArgumentException("InputFile must not be null");
}
List<ValidationResult> results = new ArrayList<>();
// Execute structure validation first
ValidationResult structureResult = structureValidator.validate(inputFile);
results.add(structureResult);
// Execute field validation
ValidationResult fieldResult = fieldValidator.validate(inputFile);
results.add(fieldResult);
// Merge all results into a single result
return ValidationResult.merge(results);
}
}

View File

@@ -0,0 +1,20 @@
package de.gecheckt.asv.validation;
import de.gecheckt.asv.domain.model.InputFile;
import de.gecheckt.asv.validation.model.ValidationResult;
/**
* Interface for orchestrating the validation of an ASV input file.
* This validator coordinates multiple specialized validators to perform a complete validation.
*/
public interface InputFileValidator {
/**
* Validates the given input file using all configured validators.
*
* @param inputFile the input file to validate (must not be null)
* @return a validation result containing all errors found by all validators
* @throws IllegalArgumentException if inputFile is null
*/
ValidationResult validate(InputFile inputFile);
}

View File

@@ -0,0 +1,60 @@
package de.gecheckt.asv.validation.example;
import de.gecheckt.asv.validation.model.ValidationError;
import de.gecheckt.asv.validation.model.ValidationResult;
import de.gecheckt.asv.validation.model.ValidationSeverity;
import java.util.Arrays;
import java.util.List;
/**
* Beispielanwendung zur Demonstration der Verwendung der Validierungsmodelle.
*/
public class ValidationExample {
public static void main(String[] args) {
// Erstelle einige Beispielvalidierungsfehler
ValidationError error1 = new ValidationError(
"FORMAT001",
"Ungültiges Datumsformat",
ValidationSeverity.ERROR,
"KOPF",
1,
"ERSTELLUNGSDATUM",
3,
"2023-99-99",
"YYYY-MM-DD"
);
ValidationError warning1 = new ValidationError(
"LENGTH001",
"Feldlänge überschritten",
ValidationSeverity.WARNING,
"POSITION",
5,
"ARTIKELBEZEICHNUNG",
2,
"Extrem langer Artikelname, der die maximale Feldlänge überschreitet",
"Max. 30 Zeichen"
);
ValidationError info1 = new ValidationError(
"OPTIONAL001",
"Optionales Feld ist leer",
ValidationSeverity.INFO,
"FUSS",
100,
"BEMERKUNG",
1,
null,
"Freitextfeld für zusätzliche Informationen"
);
// Erstelle ein ValidationResult-Objekt
List<ValidationError> validationErrors = Arrays.asList(error1, warning1, info1);
ValidationResult result = new ValidationResult(validationErrors);
// Gib das Ergebnis auf der Konsole aus
result.printToConsole();
}
}

View File

@@ -0,0 +1,248 @@
package de.gecheckt.asv.validation.field;
import java.util.ArrayList;
import java.util.List;
import de.gecheckt.asv.domain.model.Field;
import de.gecheckt.asv.domain.model.InputFile;
import de.gecheckt.asv.domain.model.Message;
import de.gecheckt.asv.domain.model.Segment;
import de.gecheckt.asv.validation.model.ValidationError;
import de.gecheckt.asv.validation.model.ValidationResult;
import de.gecheckt.asv.validation.model.ValidationSeverity;
/**
* Default implementation of FieldValidator that checks general field rules.
*
* Rules checked:
* 1. Field.rawValue must not be null
* 2. Field.rawValue must not be empty
* 3. Field.rawValue must not consist only of whitespaces
* 4. fieldPosition must be positive
* 5. Field positions within a segment should be consecutive without gaps, starting at 1
* 6. If fieldName is set, it must not be empty or only whitespace
*/
public class DefaultFieldValidator implements FieldValidator {
@Override
public ValidationResult validate(InputFile inputFile) {
if (inputFile == null) {
throw new IllegalArgumentException("InputFile must not be null");
}
var errors = new ArrayList<ValidationError>();
// Process all messages in the input file
for (var message : inputFile.messages()) {
// Process all segments in each message
for (var segment : message.segments()) {
// Validate fields in this segment
validateFields(segment.fields(), segment.segmentName(), segment.segmentPosition(), errors);
}
}
return new ValidationResult(errors);
}
/**
* Validates all fields in a segment according to field validation rules.
*
* @param fields the list of fields to validate
* @param segmentName the name of the parent segment
* @param segmentPosition the position of the parent segment
* @param errors the list to add validation errors to
*/
private void validateFields(List<Field> fields, String segmentName, int segmentPosition, List<ValidationError> errors) {
// Process each field
for (var field : fields) {
validateSingleField(field, segmentName, segmentPosition, errors);
}
// Check for consecutive field positions
validateConsecutiveFieldPositions(fields, segmentName, segmentPosition, errors);
}
/**
* Validates a single field according to field validation rules.
*
* @param field the field to validate
* @param segmentName the name of the parent segment
* @param segmentPosition the position of the parent segment
* @param errors the list to add validation errors to
*/
private void validateSingleField(Field field, String segmentName, int segmentPosition, List<ValidationError> errors) {
var rawValue = field.rawValue();
var fieldPosition = field.fieldPosition();
var fieldName = field.getFieldName().orElse("");
// Rule 1: Field.rawValue must not be null
// (This is already enforced by the domain model, but we check for completeness)
if (rawValue == null) {
errors.add(createError(
"FIELD_001",
"Field raw value must not be null",
ValidationSeverity.ERROR,
segmentName,
segmentPosition,
fieldName,
fieldPosition,
"null",
"Non-null raw value required"
));
} else {
// Rule 2: Field.rawValue must not be empty
if (rawValue.isEmpty()) {
errors.add(createError(
"FIELD_002",
"Field raw value must not be empty",
ValidationSeverity.ERROR,
segmentName,
segmentPosition,
fieldName,
fieldPosition,
rawValue,
"Non-empty raw value required"
));
}
// Rule 3: Field.rawValue must not consist only of whitespaces
if (rawValue.trim().isEmpty() && !rawValue.isEmpty()) {
errors.add(createError(
"FIELD_003",
"Field raw value must not consist only of whitespaces",
ValidationSeverity.ERROR,
segmentName,
segmentPosition,
fieldName,
fieldPosition,
rawValue,
"Non-whitespace-only raw value required"
));
}
}
// Rule 4: fieldPosition must be positive
// (This is already enforced by the domain model, but we check for completeness)
if (fieldPosition <= 0) {
errors.add(createError(
"FIELD_004",
"Field position must be positive",
ValidationSeverity.ERROR,
segmentName,
segmentPosition,
fieldName,
fieldPosition,
String.valueOf(fieldPosition),
"Positive field position required"
));
}
// Rule 6: If fieldName is set, it must not be empty or only whitespace
if (field.getFieldName().isPresent()) {
var name = field.getFieldName().get();
if (name.isEmpty()) {
errors.add(createError(
"FIELD_006",
"Field name must not be empty",
ValidationSeverity.ERROR,
segmentName,
segmentPosition,
name,
fieldPosition,
name,
"Non-empty field name required"
));
} else if (name.trim().isEmpty()) {
errors.add(createError(
"FIELD_006",
"Field name must not consist only of whitespaces",
ValidationSeverity.ERROR,
segmentName,
segmentPosition,
name,
fieldPosition,
name,
"Non-whitespace-only field name required"
));
}
}
}
/**
* Validates that field positions within a segment are consecutive without gaps, starting at 1.
*
* @param fields the list of fields to validate
* @param segmentName the name of the parent segment
* @param segmentPosition the position of the parent segment
* @param errors the list to add validation errors to
*/
private void validateConsecutiveFieldPositions(List<Field> fields, String segmentName, int segmentPosition, List<ValidationError> errors) {
if (fields.isEmpty()) {
return;
}
// Find the maximum field position to determine the expected range
var maxPosition = fields.stream()
.mapToInt(Field::fieldPosition)
.max()
.orElse(0);
// Check for gaps in field positions
for (int i = 1; i <= maxPosition; i++) {
final var position = i;
var positionExists = fields.stream()
.anyMatch(f -> f.fieldPosition() == position);
if (!positionExists) {
// Rule 5: Field positions within a segment should be consecutive without gaps, starting at 1
errors.add(createError(
"FIELD_005",
"Missing field at position " + position + " - field positions should be consecutive",
ValidationSeverity.WARNING,
segmentName,
segmentPosition,
"",
position,
"",
"Consecutive field positions starting at 1 required"
));
}
}
}
/**
* Helper method to create a ValidationError with consistent parameters.
*
* @param errorCode the error code
* @param description the error description
* @param severity the validation severity
* @param segmentName the segment name
* @param segmentPosition the segment position
* @param fieldName the field name
* @param fieldPosition the field position
* @param actualValue the actual value
* @param expectedRule the expected rule
* @return a new ValidationError instance
*/
private ValidationError createError(String errorCode,
String description,
ValidationSeverity severity,
String segmentName,
int segmentPosition,
String fieldName,
int fieldPosition,
String actualValue,
String expectedRule) {
return new ValidationError(
errorCode,
description,
severity,
segmentName != null ? segmentName : "",
segmentPosition,
fieldName != null ? fieldName : "",
fieldPosition,
actualValue,
expectedRule
);
}
}

View File

@@ -0,0 +1,20 @@
package de.gecheckt.asv.validation.field;
import de.gecheckt.asv.domain.model.InputFile;
import de.gecheckt.asv.validation.model.ValidationResult;
/**
* Interface for validating fields in an ASV input file.
* This validator checks general field rules without requiring specification details.
*/
public interface FieldValidator {
/**
* Validates the fields in the given input file.
*
* @param inputFile the input file to validate (must not be null)
* @return a validation result containing any field errors found
* @throws IllegalArgumentException if inputFile is null
*/
ValidationResult validate(InputFile inputFile);
}

View File

@@ -0,0 +1,58 @@
package de.gecheckt.asv.validation.model;
import java.util.Objects;
import java.util.Optional;
/**
* Repräsentiert einen einzelnen Validierungsfehler mit allen relevanten Informationen.
* Diese Klasse ist unveränderlich (immutable).
*
* @param errorCode Fehlercode oder Fehlerart (darf nicht null sein)
* @param description verständliche Beschreibung (darf nicht null sein)
* @param severity Prüfstufe (darf nicht null sein)
* @param segmentName Segmentname oder Segmentkennung (darf nicht null sein)
* @param segmentPosition Segmentposition
* @param fieldName Feldname (darf nicht null sein)
* @param fieldPosition Feldposition
* @param actualValue optionaler Ist-Wert (kann null sein)
* @param expectedRule optionale Soll-Regel bzw. Erwartung (kann null sein)
*/
public record ValidationError(String errorCode,
String description,
ValidationSeverity severity,
String segmentName,
int segmentPosition,
String fieldName,
int fieldPosition,
String actualValue,
String expectedRule) {
/**
* Konstruktor für ValidationError.
*
* @param errorCode Fehlercode oder Fehlerart (darf nicht null sein)
* @param description verständliche Beschreibung (darf nicht null sein)
* @param severity Prüfstufe (darf nicht null sein)
* @param segmentName Segmentname oder Segmentkennung (darf nicht null sein)
* @param segmentPosition Segmentposition
* @param fieldName Feldname (darf nicht null sein)
* @param fieldPosition Feldposition
* @param actualValue optionaler Ist-Wert (kann null sein)
* @param expectedRule optionale Soll-Regel bzw. Erwartung (kann null sein)
*/
public ValidationError {
errorCode = Objects.requireNonNull(errorCode, "errorCode must not be null");
description = Objects.requireNonNull(description, "description must not be null");
severity = Objects.requireNonNull(severity, "severity must not be null");
segmentName = Objects.requireNonNull(segmentName, "segmentName must not be null");
fieldName = Objects.requireNonNull(fieldName, "fieldName must not be null");
}
public Optional<String> getActualValue() {
return Optional.ofNullable(actualValue);
}
public Optional<String> getExpectedRule() {
return Optional.ofNullable(expectedRule);
}
}

View File

@@ -0,0 +1,184 @@
package de.gecheckt.asv.validation.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Repräsentiert das Ergebnis einer Validierung mit allen gefundenen Fehlern, Warnungen und Infos.
* Diese Klasse ist unveränderlich (immutable).
*/
public final class ValidationResult {
private final List<ValidationError> errors;
/**
* Konstruktor für ValidationResult.
*
* @param errors Liste von Validierungsfehler (darf nicht null sein)
*/
public ValidationResult(List<ValidationError> errors) {
this.errors = List.copyOf(Objects.requireNonNull(errors, "errors must not be null"));
}
/**
* Erstellt ein neues ValidationResult durch Zusammenführen mehrerer ValidationResult-Instanzen.
*
* @param results Liste von ValidationResult-Instanzen (darf nicht null sein)
* @return neues ValidationResult mit allen Fehlern aus den übergebenen Ergebnissen
*/
public static ValidationResult merge(List<ValidationResult> results) {
Objects.requireNonNull(results, "results must not be null");
var mergedErrors = results.stream()
.filter(Objects::nonNull)
.flatMap(result -> result.getAllErrors().stream())
.toList();
return new ValidationResult(mergedErrors);
}
/**
* Prüft, ob das Validierungsergebnis Fehler enthält.
*
* @return true, wenn mindestens ein Fehler vom Typ ERROR vorhanden ist, sonst false
*/
public boolean hasErrors() {
return errors.stream()
.anyMatch(error -> error.severity() == ValidationSeverity.ERROR);
}
/**
* Prüft, ob das Validierungsergebnis Warnungen enthält.
*
* @return true, wenn mindestens ein Fehler vom Typ WARNING vorhanden ist, sonst false
*/
public boolean hasWarnings() {
return errors.stream()
.anyMatch(error -> error.severity() == ValidationSeverity.WARNING);
}
/**
* Prüft, ob das Validierungsergebnis Informationen enthält.
*
* @return true, wenn mindestens ein Fehler vom Typ INFO vorhanden ist, sonst false
*/
public boolean hasInfos() {
return errors.stream()
.anyMatch(error -> error.severity() == ValidationSeverity.INFO);
}
/**
* Liefert alle Fehler vom Typ ERROR.
*
* @return unveränderliche Liste von Fehlern
*/
public List<ValidationError> getErrors() {
return errors.stream()
.filter(error -> error.severity() == ValidationSeverity.ERROR)
.toList();
}
/**
* Liefert alle Fehler vom Typ WARNING.
*
* @return unveränderliche Liste von Warnungen
*/
public List<ValidationError> getWarnings() {
return errors.stream()
.filter(error -> error.severity() == ValidationSeverity.WARNING)
.toList();
}
/**
* Liefert alle Fehler vom Typ INFO.
*
* @return unveränderliche Liste von Informationen
*/
public List<ValidationError> getInfos() {
return errors.stream()
.filter(error -> error.severity() == ValidationSeverity.INFO)
.toList();
}
/**
* Liefert alle Validierungsfehler.
*
* @return unveränderliche Liste aller Fehler
*/
public List<ValidationError> getAllErrors() {
return errors;
}
/**
* Gibt eine textbasierte Darstellung des Validierungsergebnisses auf der Konsole aus.
*/
public void printToConsole() {
System.out.println("=== Validierungsergebnis ===");
if (hasErrors()) {
System.out.println("Fehler (" + getErrors().size() + "):");
getErrors().forEach(error -> System.out.println(" [ERROR] " + formatError(error)));
}
if (hasWarnings()) {
System.out.println("Warnungen (" + getWarnings().size() + "):");
getWarnings().forEach(error -> System.out.println(" [WARNING] " + formatError(error)));
}
if (hasInfos()) {
System.out.println("Informationen (" + getInfos().size() + "):");
getInfos().forEach(error -> System.out.println(" [INFO] " + formatError(error)));
}
if (!hasErrors() && !hasWarnings() && !hasInfos()) {
System.out.println("Keine Probleme gefunden.");
}
System.out.println("============================");
}
/**
* Formatiert einen ValidationError für die Konsolenausgabe.
*
* @param error der zu formatierende Fehler
* @return formatierte Zeichenkette
*/
private String formatError(ValidationError error) {
var sb = new StringBuilder();
sb.append(error.description());
sb.append(" (Code: ").append(error.errorCode()).append(")");
sb.append(" im Segment '").append(error.segmentName()).append("' Position ").append(error.segmentPosition());
sb.append(", Feld '").append(error.fieldName()).append("' Position ").append(error.fieldPosition());
error.getActualValue().ifPresent(value ->
sb.append(", Ist-Wert: '").append(value).append("'"));
error.getExpectedRule().ifPresent(rule ->
sb.append(", Erwartet: '").append(rule).append("'"));
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ValidationResult that = (ValidationResult) o;
return Objects.equals(errors, that.errors);
}
@Override
public int hashCode() {
return Objects.hash(errors);
}
@Override
public String toString() {
return "ValidationResult{" +
"errors=" + errors +
'}';
}
}

View File

@@ -0,0 +1,16 @@
package de.gecheckt.asv.validation.model;
/**
* Repräsentiert die Schweregrade von Validierungsergebnissen.
*/
public enum ValidationSeverity {
/** Information - kein Problem, aber erwähnenswert */
INFO,
/** Warnung - potenzielles Problem */
WARNING,
/** Fehler - schwerwiegendes Problem */
ERROR
}

View File

@@ -0,0 +1,221 @@
package de.gecheckt.asv.validation.structure;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import de.gecheckt.asv.domain.model.Field;
import de.gecheckt.asv.domain.model.InputFile;
import de.gecheckt.asv.domain.model.Message;
import de.gecheckt.asv.domain.model.Segment;
import de.gecheckt.asv.validation.model.ValidationError;
import de.gecheckt.asv.validation.model.ValidationResult;
import de.gecheckt.asv.validation.model.ValidationSeverity;
/**
* Default implementation of StructureValidator that checks general structural rules.
*
* Rules checked:
* 1. InputFile must contain at least one Message
* 2. Each Message must contain at least one Segment
* 3. Segment names must not be empty
* 4. Field positions within a Segment must be unique and positive
* 5. Segment positions within a Message must be unique and positive
* 6. Message positions within an InputFile must be unique and positive
*/
public class DefaultStructureValidator implements StructureValidator {
@Override
public ValidationResult validate(InputFile inputFile) {
if (inputFile == null) {
throw new IllegalArgumentException("InputFile must not be null");
}
var errors = new ArrayList<ValidationError>();
// Rule 1: InputFile must contain at least one Message
if (inputFile.messages().isEmpty()) {
errors.add(createError(
"STRUCTURE_001",
"Input file must contain at least one message",
ValidationSeverity.ERROR,
"",
0,
"",
0,
"",
"At least one message required"
));
} else {
// Process messages if they exist
validateMessages(inputFile.messages(), errors);
}
return new ValidationResult(errors);
}
/**
* Validates all messages in the input file.
*
* @param messages the list of messages to validate
* @param errors the list to add validation errors to
*/
private void validateMessages(List<Message> messages, List<ValidationError> errors) {
var messagePositions = new HashSet<Integer>();
for (var message : messages) {
var messagePosition = message.messagePosition();
// Rule 6: Message positions must be unique and positive
if (!messagePositions.add(messagePosition)) {
errors.add(createError(
"STRUCTURE_006",
"Duplicate message position: " + messagePosition,
ValidationSeverity.ERROR,
"",
messagePosition,
"",
0,
String.valueOf(messagePosition),
"Unique positive message positions required"
));
}
// Validate segments in this message
validateSegments(message.segments(), messagePosition, errors);
}
}
/**
* Validates all segments in a message.
*
* @param segments the list of segments to validate
* @param messagePosition the position of the parent message
* @param errors the list to add validation errors to
*/
private void validateSegments(List<Segment> segments, int messagePosition, List<ValidationError> errors) {
// Rule 2: Each Message must contain at least one Segment
if (segments.isEmpty()) {
errors.add(createError(
"STRUCTURE_002",
"Message must contain at least one segment",
ValidationSeverity.ERROR,
"",
messagePosition,
"",
0,
"",
"At least one segment required per message"
));
return; // No need to validate segments if there are none
}
var segmentPositions = new HashSet<Integer>();
for (var segment : segments) {
var segmentName = segment.segmentName();
var segmentPosition = segment.segmentPosition();
// Rule 3: Segment names must not be empty
if (segmentName == null || segmentName.isEmpty()) {
errors.add(createError(
"STRUCTURE_003",
"Segment name must not be empty",
ValidationSeverity.ERROR,
segmentName != null ? segmentName : "",
segmentPosition,
"",
0,
segmentName != null ? segmentName : "null",
"Non-empty segment name required"
));
}
// Rule 5: Segment positions must be unique and positive
if (!segmentPositions.add(segmentPosition)) {
errors.add(createError(
"STRUCTURE_005",
"Duplicate segment position: " + segmentPosition,
ValidationSeverity.ERROR,
segmentName,
segmentPosition,
"",
0,
String.valueOf(segmentPosition),
"Unique positive segment positions required"
));
}
// Validate fields in this segment
validateFields(segment.fields(), segmentName, segmentPosition, errors);
}
}
/**
* Validates all fields in a segment.
*
* @param fields the list of fields to validate
* @param segmentName the name of the parent segment
* @param segmentPosition the position of the parent segment
* @param errors the list to add validation errors to
*/
private void validateFields(List<Field> fields, String segmentName, int segmentPosition, List<ValidationError> errors) {
var fieldPositions = new HashSet<Integer>();
for (var field : fields) {
var fieldPosition = field.fieldPosition();
// Rule 4: Field positions must be unique and positive
if (!fieldPositions.add(fieldPosition)) {
errors.add(createError(
"STRUCTURE_004",
"Duplicate field position: " + fieldPosition,
ValidationSeverity.ERROR,
segmentName,
segmentPosition,
field.getFieldName().orElse(""),
fieldPosition,
String.valueOf(fieldPosition),
"Unique positive field positions required"
));
}
}
}
/**
* Helper method to create a ValidationError with consistent parameters.
*
* @param errorCode the error code
* @param description the error description
* @param severity the validation severity
* @param segmentName the segment name
* @param segmentPosition the segment position
* @param fieldName the field name
* @param fieldPosition the field position
* @param actualValue the actual value
* @param expectedRule the expected rule
* @return a new ValidationError instance
*/
private ValidationError createError(String errorCode,
String description,
ValidationSeverity severity,
String segmentName,
int segmentPosition,
String fieldName,
int fieldPosition,
String actualValue,
String expectedRule) {
return new ValidationError(
errorCode,
description,
severity,
segmentName != null ? segmentName : "",
segmentPosition,
fieldName != null ? fieldName : "",
fieldPosition,
actualValue,
expectedRule
);
}
}

View File

@@ -0,0 +1,20 @@
package de.gecheckt.asv.validation.structure;
import de.gecheckt.asv.domain.model.InputFile;
import de.gecheckt.asv.validation.model.ValidationResult;
/**
* Interface for validating the structural integrity of an ASV input file.
* This validator checks general structural rules without requiring specification details.
*/
public interface StructureValidator {
/**
* Validates the structural integrity of the given input file.
*
* @param inputFile the input file to validate (must not be null)
* @return a validation result containing any structural errors found
* @throws IllegalArgumentException if inputFile is null
*/
ValidationResult validate(InputFile inputFile);
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>