From c4853a53c91e288dfe4b16cc8f172369f77e078d Mon Sep 17 00:00:00 2001 From: Vitaliy Velikodniy Date: Sat, 18 May 2024 15:26:46 +0300 Subject: [PATCH] support for custom predicate for mixed lines --- README.md | 14 ++++- .../vitaliy/fixedlength/FixedLength.java | 53 ++++++++++++++----- .../fixedlength/annotation/FixedLine.java | 17 +++++- src/test/java/Row.java | 2 - .../vitaliy/fixedlength}/AbstractPerson.java | 3 +- .../vitaliy/fixedlength}/CatMixed.java | 3 +- .../vitaliy/fixedlength}/Employee.java | 3 +- .../vitaliy/fixedlength}/EmployeeMixed.java | 3 +- .../vitaliy/fixedlength/EmployeePosition.java | 15 ++++++ .../EmployeePositionPredicate.java | 11 ++++ .../EmployeeWithEmptyAnnotation.java | 13 +++++ .../vitaliy/fixedlength}/FormatterTest.java | 3 +- .../vitaliy/fixedlength}/HeaderSplit.java | 2 + .../fixedlength}/InheritedEmployee.java | 3 +- .../vitaliy/fixedlength}/ParserTest.java | 50 +++++++++++++---- .../velikodniy/vitaliy/fixedlength/Row.java | 4 ++ 16 files changed, 164 insertions(+), 35 deletions(-) delete mode 100644 src/test/java/Row.java rename src/test/java/{ => name/velikodniy/vitaliy/fixedlength}/AbstractPerson.java (84%) rename src/test/java/{ => name/velikodniy/vitaliy/fixedlength}/CatMixed.java (89%) rename src/test/java/{ => name/velikodniy/vitaliy/fixedlength}/Employee.java (92%) rename src/test/java/{ => name/velikodniy/vitaliy/fixedlength}/EmployeeMixed.java (93%) create mode 100644 src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeePosition.java create mode 100644 src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeePositionPredicate.java create mode 100644 src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeeWithEmptyAnnotation.java rename src/test/java/{ => name/velikodniy/vitaliy/fixedlength}/FormatterTest.java (95%) rename src/test/java/{ => name/velikodniy/vitaliy/fixedlength}/HeaderSplit.java (91%) rename src/test/java/{ => name/velikodniy/vitaliy/fixedlength}/InheritedEmployee.java (90%) rename src/test/java/{ => name/velikodniy/vitaliy/fixedlength}/ParserTest.java (81%) create mode 100644 src/test/java/name/velikodniy/vitaliy/fixedlength/Row.java diff --git a/README.md b/README.md index 14555c4..2ae7139 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ CatSnowball EmplJoe3 Smith ``` -with this files: +with these files: ```java @FixedLine(startsWith = "Empl") @@ -137,6 +137,8 @@ public class EmployeeMixed { } ``` +(fields could be final as well). + ```java @FixedLine(startsWith = "Cat") public class CatMixed { @@ -231,6 +233,16 @@ public class HeaderSplit { } ``` +## Custom rules for mixed lines + +There is a `startsWith` parameter for easy-to-use identifying the class to deserialize, but sometimes it is not enough. So there is a `predicate` parameter in `FixedLine` annotation where you should pass your own custom rule as predicate. Just implement `Predicate` and pass pointer to class in annotation. + +```java +@FixedLine(predicate = EmployeePositionPredicate.class) +``` + +This class will be initialized just once and cached. + ## Benchmark There is a benchmark, you can run it with `gradle jmh` command. Also, you can change running parameters of it in file `src/jmh/java/name/velikodniy/vitaliy/fixedlength/benchmark/BenchmarkRunner.java`. diff --git a/src/main/java/name/velikodniy/vitaliy/fixedlength/FixedLength.java b/src/main/java/name/velikodniy/vitaliy/fixedlength/FixedLength.java index 14d709a..b1309df 100644 --- a/src/main/java/name/velikodniy/vitaliy/fixedlength/FixedLength.java +++ b/src/main/java/name/velikodniy/vitaliy/fixedlength/FixedLength.java @@ -17,12 +17,15 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Scanner; import java.util.Spliterator; import java.util.Spliterators; +import java.util.function.Predicate; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -40,6 +43,7 @@ public class FixedLength { Class> > FORMATTERS = Formatter.getDefaultFormatters(); + private final Map>, Predicate> predicates = new HashMap<>(); private final List> lineTypes = new ArrayList<>(); private boolean skipUnknownLines = true; private boolean skipErroneousFields = false; @@ -53,7 +57,8 @@ private FixedFormatLine classToLineDesc(final Class clazz) { fixedFormatLine.clazz = clazz; FixedLine annotation = clazz.getDeclaredAnnotation(FixedLine.class); if (annotation != null) { - fixedFormatLine.startsWith = annotation.startsWith(); + fixedFormatLine.setStartsWith(annotation.startsWith()); + fixedFormatLine.predicate = annotation.predicate(); } for (Field field : getAllFields(clazz)) { FixedField fieldAnnotation = field.getDeclaredAnnotation(FixedField.class); @@ -150,21 +155,32 @@ public FixedLength usingLineDelimiter(String delimiterString) { return this; } - private FixedFormatRecord fixedFormatLine(String line) { - if (lineTypes.size() == 1) { - if (lineTypes.get(0).startsWith == null) { - return new FixedFormatRecord(line, lineTypes.get(0)); - } else if (line.startsWith(lineTypes.get(0).startsWith)) { - return new FixedFormatRecord(line, lineTypes.get(0)); - } else { - return null; + private Predicate getPredicate(Class> clazz) { + if (predicates.containsKey(clazz)) { + return predicates.get(clazz); + } else { + Predicate predicate; + try { + predicate = clazz.getDeclaredConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + throw new FixedLengthException("Cannot init predicate, it should have empty constructor", e); } + predicates.put(clazz, predicate); + return predicate; } + } + + private FixedFormatRecord fixedFormatLine(String line) { for (FixedFormatLine lineType : lineTypes) { if ( - lineType.startsWith != null - && - line.startsWith(lineType.startsWith) + lineType.getStartsWith() + .map(line::startsWith) + .orElse(true) && + lineType.getPredicate() + .map(this::getPredicate) + .map(p -> p.test(line)) + .orElse(true) ) { return new FixedFormatRecord(line, lineType); } @@ -386,18 +402,27 @@ private FixedFormatRecord( private static class FixedFormatLine { private String startsWith = null; + private Class> predicate; private Class clazz; private final List fixedFormatFields = new ArrayList<>(); private Method splitAfterMethod; - public String getStartsWith() { - return startsWith; + public Optional getStartsWith() { + return Optional.ofNullable(startsWith).flatMap(s -> s.isEmpty() ? Optional.empty() : Optional.of(s)); + } + + public Optional>> getPredicate() { + return Optional.ofNullable(predicate); } public void setStartsWith(String startsWith) { this.startsWith = startsWith; } + public void setPredicate(Class> predicate) { + this.predicate = predicate; + } + public Class getClazz() { return clazz; } diff --git a/src/main/java/name/velikodniy/vitaliy/fixedlength/annotation/FixedLine.java b/src/main/java/name/velikodniy/vitaliy/fixedlength/annotation/FixedLine.java index d354a22..84e80f9 100644 --- a/src/main/java/name/velikodniy/vitaliy/fixedlength/annotation/FixedLine.java +++ b/src/main/java/name/velikodniy/vitaliy/fixedlength/annotation/FixedLine.java @@ -4,6 +4,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.function.Predicate; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @@ -13,5 +14,19 @@ * * @return Indicator of line type */ - String startsWith(); + String startsWith() default ""; + + /** + * Predicate to check if the line is of this type. + * + * @return Predicate to check the line type. + */ + Class> predicate() default DefaultPredicate.class; + + class DefaultPredicate implements Predicate { + @Override + public boolean test(String line) { + return true; // Default predicate always returns true + } + } } diff --git a/src/test/java/Row.java b/src/test/java/Row.java deleted file mode 100644 index 5c0a144..0000000 --- a/src/test/java/Row.java +++ /dev/null @@ -1,2 +0,0 @@ -public interface Row { -} diff --git a/src/test/java/AbstractPerson.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/AbstractPerson.java similarity index 84% rename from src/test/java/AbstractPerson.java rename to src/test/java/name/velikodniy/vitaliy/fixedlength/AbstractPerson.java index 10ffcc2..212cd3c 100644 --- a/src/test/java/AbstractPerson.java +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/AbstractPerson.java @@ -1,4 +1,5 @@ -import name.velikodniy.vitaliy.fixedlength.Align; +package name.velikodniy.vitaliy.fixedlength; + import name.velikodniy.vitaliy.fixedlength.annotation.FixedField; public abstract class AbstractPerson { diff --git a/src/test/java/CatMixed.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/CatMixed.java similarity index 89% rename from src/test/java/CatMixed.java rename to src/test/java/name/velikodniy/vitaliy/fixedlength/CatMixed.java index 5d1d26d..ee5e36a 100644 --- a/src/test/java/CatMixed.java +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/CatMixed.java @@ -1,4 +1,5 @@ -import name.velikodniy.vitaliy.fixedlength.Align; +package name.velikodniy.vitaliy.fixedlength; + import name.velikodniy.vitaliy.fixedlength.annotation.FixedField; import name.velikodniy.vitaliy.fixedlength.annotation.FixedLine; diff --git a/src/test/java/Employee.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/Employee.java similarity index 92% rename from src/test/java/Employee.java rename to src/test/java/name/velikodniy/vitaliy/fixedlength/Employee.java index f160535..f3406c5 100644 --- a/src/test/java/Employee.java +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/Employee.java @@ -1,4 +1,5 @@ -import name.velikodniy.vitaliy.fixedlength.Align; +package name.velikodniy.vitaliy.fixedlength; + import name.velikodniy.vitaliy.fixedlength.annotation.FixedField; import java.math.BigDecimal; diff --git a/src/test/java/EmployeeMixed.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeeMixed.java similarity index 93% rename from src/test/java/EmployeeMixed.java rename to src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeeMixed.java index cb1a303..740941e 100644 --- a/src/test/java/EmployeeMixed.java +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeeMixed.java @@ -1,4 +1,5 @@ -import name.velikodniy.vitaliy.fixedlength.Align; +package name.velikodniy.vitaliy.fixedlength; + import name.velikodniy.vitaliy.fixedlength.annotation.FixedField; import name.velikodniy.vitaliy.fixedlength.annotation.FixedLine; diff --git a/src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeePosition.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeePosition.java new file mode 100644 index 0000000..e351e80 --- /dev/null +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeePosition.java @@ -0,0 +1,15 @@ +package name.velikodniy.vitaliy.fixedlength; + +import name.velikodniy.vitaliy.fixedlength.annotation.FixedField; +import name.velikodniy.vitaliy.fixedlength.annotation.FixedLine; + +@FixedLine(predicate = EmployeePositionPredicate.class) +public class EmployeePosition { + + @FixedField(offset = 1, length = 10, align = Align.LEFT) + private String position; + + public String getPosition() { + return position; + } +} diff --git a/src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeePositionPredicate.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeePositionPredicate.java new file mode 100644 index 0000000..db2fd97 --- /dev/null +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeePositionPredicate.java @@ -0,0 +1,11 @@ +package name.velikodniy.vitaliy.fixedlength; + +import java.util.function.Predicate; + +public class EmployeePositionPredicate implements Predicate { + + @Override + public boolean test(String s) { + return s.contains("POSITION"); + } +} \ No newline at end of file diff --git a/src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeeWithEmptyAnnotation.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeeWithEmptyAnnotation.java new file mode 100644 index 0000000..32cb481 --- /dev/null +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/EmployeeWithEmptyAnnotation.java @@ -0,0 +1,13 @@ +package name.velikodniy.vitaliy.fixedlength; + +import name.velikodniy.vitaliy.fixedlength.annotation.FixedField; +import name.velikodniy.vitaliy.fixedlength.annotation.FixedLine; + +@FixedLine +public class EmployeeWithEmptyAnnotation implements Row { + @FixedField(offset = 1, length = 10, align = Align.LEFT) + public String firstName; + + @FixedField(offset = 11, length = 10, align = Align.LEFT) + String lastName; +} diff --git a/src/test/java/FormatterTest.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/FormatterTest.java similarity index 95% rename from src/test/java/FormatterTest.java rename to src/test/java/name/velikodniy/vitaliy/fixedlength/FormatterTest.java index 0c74440..e95ea6b 100644 --- a/src/test/java/FormatterTest.java +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/FormatterTest.java @@ -1,4 +1,5 @@ -import name.velikodniy.vitaliy.fixedlength.FixedLength; +package name.velikodniy.vitaliy.fixedlength; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/HeaderSplit.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/HeaderSplit.java similarity index 91% rename from src/test/java/HeaderSplit.java rename to src/test/java/name/velikodniy/vitaliy/fixedlength/HeaderSplit.java index ade2b68..3b9e0aa 100644 --- a/src/test/java/HeaderSplit.java +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/HeaderSplit.java @@ -1,3 +1,5 @@ +package name.velikodniy.vitaliy.fixedlength; + import name.velikodniy.vitaliy.fixedlength.annotation.FixedField; import name.velikodniy.vitaliy.fixedlength.annotation.FixedLine; import name.velikodniy.vitaliy.fixedlength.annotation.SplitLineAfter; diff --git a/src/test/java/InheritedEmployee.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/InheritedEmployee.java similarity index 90% rename from src/test/java/InheritedEmployee.java rename to src/test/java/name/velikodniy/vitaliy/fixedlength/InheritedEmployee.java index 56741ac..40368b3 100644 --- a/src/test/java/InheritedEmployee.java +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/InheritedEmployee.java @@ -1,4 +1,5 @@ -import name.velikodniy.vitaliy.fixedlength.Align; +package name.velikodniy.vitaliy.fixedlength; + import name.velikodniy.vitaliy.fixedlength.annotation.FixedField; import java.math.BigDecimal; diff --git a/src/test/java/ParserTest.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/ParserTest.java similarity index 81% rename from src/test/java/ParserTest.java rename to src/test/java/name/velikodniy/vitaliy/fixedlength/ParserTest.java index 002c119..140b029 100644 --- a/src/test/java/ParserTest.java +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/ParserTest.java @@ -1,5 +1,5 @@ -import name.velikodniy.vitaliy.fixedlength.FixedLength; -import name.velikodniy.vitaliy.fixedlength.FixedLengthException; +package name.velikodniy.vitaliy.fixedlength; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,7 +13,10 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; class ParserTest { @@ -38,14 +41,18 @@ class ParserTest { String mixedTypesWrongSplitRecordExample = "HEADERMy Title 00 EmplJoe1 Smith Developer 07500010012009\n" + - "CatSnowball 20200103\n" + - "EmplJoe3 Smith Developer "; + "CatSnowball 20200103\n" + + "EmplJoe3 Smith Developer "; String mixedTypesCustomDelimiter = "EmplJoe1 Smith Developer 07500010012009@" + "CatSnowball 20200103@" + "EmplJoe3 Smith Developer "; + String mixedTypesCustomExample = + "EmplJoe1 Smith Developer 07500010012009\n" + + "Engineer POSITION"; + @Test @DisplayName("Parse as input stream with default charset and one line type") void testParseInheritedOneLineType() throws FixedLengthException { @@ -54,12 +61,12 @@ void testParseInheritedOneLineType() throws FixedLengthException { .parse(new ByteArrayInputStream(singleTypeExample.getBytes())); assertEquals(2, parse.size()); - parse.forEach( e ->{ - assertNotNull(((InheritedEmployee)e).firstName); - assertNotNull(((InheritedEmployee)e).lastName); + parse.forEach(e -> { + assertNotNull(((InheritedEmployee) e).firstName); + assertNotNull(((InheritedEmployee) e).lastName); }); } - + @Test @DisplayName("Parse as input stream with default charset and one line type") void testParseOneLineType() throws FixedLengthException { @@ -70,13 +77,23 @@ void testParseOneLineType() throws FixedLengthException { assertEquals(2, parse.size()); } + @Test + @DisplayName("Parse as input stream with default charset and one line type and empty annotation") + void testParseOneLineTypeEmptyAnnotation() throws FixedLengthException { + List parse = new FixedLength() + .registerLineType(EmployeeWithEmptyAnnotation.class) + .parse(new ByteArrayInputStream(singleTypeExample.getBytes())); + + assertEquals(2, parse.size()); + } + @Test @DisplayName("Parse as input stream with throwing exception when format erroneous fields") void testParseThrowsExceptionOnInvalidFormat() throws FixedLengthException { assertThrows(DateTimeParseException.class, () -> new FixedLength() - .registerLineType(Employee.class) - .parse(new ByteArrayInputStream(singleTypeWithErrorExample.getBytes()))); + .registerLineType(Employee.class) + .parse(new ByteArrayInputStream(singleTypeWithErrorExample.getBytes()))); } @Test @@ -188,4 +205,15 @@ void testParseReaderWithDefaultCharset() throws FixedLengthException { assertEquals(2, parse.size()); } + + @Test + @DisplayName("Parse as input stream with default charset and mixed line type and custom predicate") + void testParseMixedLineTypeCustomPredicate() throws FixedLengthException { + List parse = new FixedLength<>() + .registerLineType(EmployeeMixed.class) + .registerLineType(EmployeePosition.class) + .parse(new ByteArrayInputStream(mixedTypesCustomExample.getBytes())); + + assertEquals(2, parse.size()); + } } diff --git a/src/test/java/name/velikodniy/vitaliy/fixedlength/Row.java b/src/test/java/name/velikodniy/vitaliy/fixedlength/Row.java new file mode 100644 index 0000000..54fddb5 --- /dev/null +++ b/src/test/java/name/velikodniy/vitaliy/fixedlength/Row.java @@ -0,0 +1,4 @@ +package name.velikodniy.vitaliy.fixedlength; + +public interface Row { +}