Skip to content

Commit

Permalink
support for custom predicate for mixed lines
Browse files Browse the repository at this point in the history
  • Loading branch information
g0ddest committed Aug 20, 2024
1 parent 32e2522 commit 074425f
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 25 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ CatSnowball
EmplJoe3 Smith
```

with this files:
with these files:

```java
@FixedLine(startsWith = "Empl")
Expand All @@ -137,6 +137,8 @@ public class EmployeeMixed {
}
```

(fields could be final as well).

```java
@FixedLine(startsWith = "Cat")
public class CatMixed {
Expand Down Expand Up @@ -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<String>` 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`.
53 changes: 39 additions & 14 deletions src/main/java/name/velikodniy/vitaliy/fixedlength/FixedLength.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,6 +43,7 @@ public class FixedLength<T> {
Class<? extends Formatter<? extends Serializable>>
> FORMATTERS
= Formatter.getDefaultFormatters();
private final Map<Class<? extends Predicate<String>>, Predicate<String>> predicates = new HashMap<>();
private final List<FixedFormatLine<? extends T>> lineTypes = new ArrayList<>();
private boolean skipUnknownLines = true;
private boolean skipErroneousFields = false;
Expand All @@ -53,7 +57,8 @@ private FixedFormatLine<T> classToLineDesc(final Class<? extends T> 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);
Expand Down Expand Up @@ -150,21 +155,32 @@ public FixedLength<T> 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<String> getPredicate(Class<? extends Predicate<String>> clazz) {
if (predicates.containsKey(clazz)) {
return predicates.get(clazz);
} else {
Predicate<String> 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<? extends T> 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);
}
Expand Down Expand Up @@ -386,18 +402,27 @@ private FixedFormatRecord(

private static class FixedFormatLine<T> {
private String startsWith = null;
private Class<? extends Predicate<String>> predicate;
private Class<? extends T> clazz;
private final List<FixedFormatField> fixedFormatFields = new ArrayList<>();
private Method splitAfterMethod;

public String getStartsWith() {
return startsWith;
public Optional<String> getStartsWith() {
return Optional.ofNullable(startsWith).flatMap(s -> s.isEmpty() ? Optional.empty() : Optional.of(s));
}

public Optional<Class<? extends Predicate<String>>> getPredicate() {
return Optional.ofNullable(predicate);
}

public void setStartsWith(String startsWith) {
this.startsWith = startsWith;
}

public void setPredicate(Class<? extends Predicate<String>> predicate) {
this.predicate = predicate;
}

public Class<? extends T> getClazz() {
return clazz;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<? extends Predicate<String>> predicate() default DefaultPredicate.class;

class DefaultPredicate implements Predicate<String> {
@Override
public boolean test(String line) {
return true; // Default predicate always returns true
}
}
}
14 changes: 14 additions & 0 deletions src/test/java/EmployeePosition.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import name.velikodniy.vitaliy.fixedlength.Align;
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;
}
}
9 changes: 9 additions & 0 deletions src/test/java/EmployeePositionPredicate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import java.util.function.Predicate;

public class EmployeePositionPredicate implements Predicate<String> {

@Override
public boolean test(String s) {
return s.contains("POSITION");
}
}
12 changes: 12 additions & 0 deletions src/test/java/EmployeeWithEmptyAnnotation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import name.velikodniy.vitaliy.fixedlength.Align;
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;
}
46 changes: 37 additions & 9 deletions src/test/java/ParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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<Row> parse = new FixedLength<Row>()
.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<Row>()
.registerLineType(Employee.class)
.parse(new ByteArrayInputStream(singleTypeWithErrorExample.getBytes())));
.registerLineType(Employee.class)
.parse(new ByteArrayInputStream(singleTypeWithErrorExample.getBytes())));
}

@Test
Expand Down Expand Up @@ -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<Object> parse = new FixedLength<>()
.registerLineType(EmployeeMixed.class)
.registerLineType(EmployeePosition.class)
.parse(new ByteArrayInputStream(mixedTypesCustomExample.getBytes()));

assertEquals(2, parse.size());
}
}

0 comments on commit 074425f

Please sign in to comment.