3

Given this bean:

@Data
public class Contact {
    @PreAssignmentValidator(validator = MustMatchRegexExpression.class, paramString = "^[A-Za-z]{3,8}$")
    @CsvBindByName(column = "Contact First Name", required = true)
    private String contactFirstName;

    @PreAssignmentValidator(validator = MustMatchRegexExpression.class, paramString = "^[0-9]{10}$")
    @CsvBindByName(column = "Phone Number", required = true)
    private String phone;
}

and the CsvToBeanBuilder() configured as follows:

...
final CsvToBean<Contact> beans = new CsvToBeanBuilder<Contact>(
                    Files.newBufferedReader(csvFilePath, StandardCharsets.UTF_8))
                    .withType(Contact.class)
                    .withThrowExceptions(false)
                    .build();

this.contacts = beans.parse();

beans.getCapturedExceptions().stream().forEach(ex -> System.out.println(ex.getMessage()));
...

If I give it a file like:

Contact First Name,Phone Number
joe1,1234567890
jane,123456789

I (correctly) get these error messages:

Field userName value "joe1" did not match expected format of ^[A-Za-z]{3,8}$
Field phone value "123456789" did not match expected format of ^[0-9]{10}$

Since I am passing these messages back to the user, I would prefer if the error message used the CSV column name instead of the bean's field name, and if I can provide a custom validation message -- maybe as an additional field the @PreAssignmentValidator? -- so that the messages look like:

Field 'User Name' value "joe1" did not match expected format of '3 - 8 alphabetic characters'
Field 'Phone Number' value "123456789" did not match expected format of '10 digits'

Any suggestions/pointers on how I can do this without writing some custom logic to transform these messages?

Thank you!


Code updated based on suggestion from @andrewjames

@Getter
public class Contact {
    //@PreAssignmentValidator(validator = MustMatchRegexExpression.class, paramString = "^[A-Za-z]{3,8}$")
    @CsvBindByName(column = "Contact First Name", required = true)
    private String contactFirstName;

    //@PreAssignmentValidator(validator = MustMatchRegexExpression.class, paramString = "^[0-9]{10}$")
    @CsvBindByName(column = "Phone Number", required = true)
    private String phone;

    public void setContactFirstName(String contactFirstName) throws CsvValidationException {
        if (contactFirstName.length() < 3 || contactFirstName.length() > 8) {
            throw new CsvValidationException("'Contact First Name' must be between 3-8 characters long");
        }
        this.contactFirstName = contactFirstName;
    }

    public void setPhone(String phone) throws CsvValidationException {
        if (phone.length() != 10) {
            throw new CsvValidationException("'Phone Number' must be between 10 digits long");
        }
        this.phone = phone;
    }
}

public class ContactTest {
    private static final String HEADER = "Contact First Name,Phone Number\n";


    @Test
    public void test() throws Exception {
        String data = HEADER
                + "jo,1234567890\n"
                + "al,123456789";  // This row should generate two exceptions

        CsvToBean<Contact> csvToBean = new CsvToBeanBuilder<Contact>(new StringReader(data))
                .withType(Contact.class)
                .withThrowExceptions(false)
//                .withExceptionHandler(new ExceptionHandlerQueue())  // Tried this way after commenting previous line
                .build();

        List<Contact> beans = csvToBean.parse();

        csvToBean.getCapturedExceptions().stream().forEach((ex) -> {
                System.out.println((int) ex.getLineNumber() + " -- " + ex.getMessage());
        });
    }
}

But now the csvToBean.parse() just throws the exeptions. And For the second row, I only get the first exception:

Exception in thread "pool-1-thread-2" java.lang.RuntimeException: com.opencsv.exceptions.CsvBeanIntrospectionException: An introspection error was thrown while attempting to manipulate property contactFirstName of bean org.lotia.example.entity.Contact.
    at com.opencsv.bean.concurrent.ProcessCsvLine.run(ProcessCsvLine.java:111)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: com.opencsv.exceptions.CsvBeanIntrospectionException: An introspection error was thrown while attempting to manipulate property contactFirstName of bean org.lotia.example.entity.Contact.
    at com.opencsv.bean.AbstractBeanField.assignValueToField(AbstractBeanField.java:290)
    at com.opencsv.bean.AbstractBeanField.setFieldValue(AbstractBeanField.java:182)
    at com.opencsv.bean.AbstractMappingStrategy.setFieldValue(AbstractMappingStrategy.java:607)
    at com.opencsv.bean.AbstractMappingStrategy.populateNewBean(AbstractMappingStrategy.java:330)
    at com.opencsv.bean.concurrent.ProcessCsvLine.processLine(ProcessCsvLine.java:131)
    at com.opencsv.bean.concurrent.ProcessCsvLine.run(ProcessCsvLine.java:87)
    ... 3 more
Caused by: java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at com.opencsv.bean.FieldAccess.lambda$determineAssignmentMethod$3(FieldAccess.java:79)
    at com.opencsv.bean.FieldAccess.setField(FieldAccess.java:115)
    at com.opencsv.bean.AbstractBeanField.assignValueToField(AbstractBeanField.java:286)
    ... 8 more
Caused by: com.opencsv.exceptions.CsvValidationException: 'Contact First Name' must be between 3-8 characters long
    at org.lotia.example.entity.Contact.setContactFirstName(Contact.java:23)
    ... 15 more
Exception in thread "pool-1-thread-1" java.lang.RuntimeException: com.opencsv.exceptions.CsvBeanIntrospectionException: An introspection error was thrown while attempting to manipulate property contactFirstName of bean org.lotia.example.entity.Contact.
    at com.opencsv.bean.concurrent.ProcessCsvLine.run(ProcessCsvLine.java:111)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: com.opencsv.exceptions.CsvBeanIntrospectionException: An introspection error was thrown while attempting to manipulate property contactFirstName of bean org.lotia.example.entity.Contact.
    at com.opencsv.bean.AbstractBeanField.assignValueToField(AbstractBeanField.java:290)
    at com.opencsv.bean.AbstractBeanField.setFieldValue(AbstractBeanField.java:182)
    at com.opencsv.bean.AbstractMappingStrategy.setFieldValue(AbstractMappingStrategy.java:607)
    at com.opencsv.bean.AbstractMappingStrategy.populateNewBean(AbstractMappingStrategy.java:330)
    at com.opencsv.bean.concurrent.ProcessCsvLine.processLine(ProcessCsvLine.java:131)
    at com.opencsv.bean.concurrent.ProcessCsvLine.run(ProcessCsvLine.java:87)
    ... 3 more
Caused by: java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at com.opencsv.bean.FieldAccess.lambda$determineAssignmentMethod$3(FieldAccess.java:79)
    at com.opencsv.bean.FieldAccess.setField(FieldAccess.java:115)
    at com.opencsv.bean.AbstractBeanField.assignValueToField(AbstractBeanField.java:286)
    ... 8 more
Caused by: com.opencsv.exceptions.CsvValidationException: 'Contact First Name' must be between 3-8 characters long
    at org.lotia.example.entity.Contact.setContactFirstName(Contact.java:23)
    ... 15 more

java.lang.RuntimeException: com.opencsv.exceptions.CsvBeanIntrospectionException: An introspection error was thrown while attempting to manipulate property contactFirstName of bean org.lotia.example.entity.Contact.

    at com.opencsv.bean.concurrent.IntolerantThreadPoolExecutor.checkExceptions(IntolerantThreadPoolExecutor.java:253)
    at com.opencsv.bean.concurrent.LineExecutor.checkExceptions(LineExecutor.java:67)
    at com.opencsv.bean.concurrent.IntolerantThreadPoolExecutor.areMoreResultsAvailable(IntolerantThreadPoolExecutor.java:303)
    at com.opencsv.bean.concurrent.IntolerantThreadPoolExecutor.tryAdvance(IntolerantThreadPoolExecutor.java:313)
    at com.opencsv.bean.concurrent.LineExecutor.tryAdvance(LineExecutor.java:24)
    at java.base/java.util.Spliterator.forEachRemaining(Spliterator.java:326)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
    at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)
    at com.opencsv.bean.CsvToBean.parse(CsvToBean.java:117)
    at org.lotia.example.ContactTest.test(ContactTest.java:28)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
Caused by: com.opencsv.exceptions.CsvBeanIntrospectionException: An introspection error was thrown while attempting to manipulate property contactFirstName of bean org.lotia.example.entity.Contact.
    at com.opencsv.bean.AbstractBeanField.assignValueToField(AbstractBeanField.java:290)
    at com.opencsv.bean.AbstractBeanField.setFieldValue(AbstractBeanField.java:182)
    at com.opencsv.bean.AbstractMappingStrategy.setFieldValue(AbstractMappingStrategy.java:607)
    at com.opencsv.bean.AbstractMappingStrategy.populateNewBean(AbstractMappingStrategy.java:330)
    at com.opencsv.bean.concurrent.ProcessCsvLine.processLine(ProcessCsvLine.java:131)
    at com.opencsv.bean.concurrent.ProcessCsvLine.run(ProcessCsvLine.java:87)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at com.opencsv.bean.FieldAccess.lambda$determineAssignmentMethod$3(FieldAccess.java:79)
    at com.opencsv.bean.FieldAccess.setField(FieldAccess.java:115)
    at com.opencsv.bean.AbstractBeanField.assignValueToField(AbstractBeanField.java:286)
    ... 8 more
Caused by: com.opencsv.exceptions.CsvValidationException: 'Contact First Name' must be between 3-8 characters long
    at org.lotia.example.entity.Contact.setContactFirstName(Contact.java:23)
    ... 15 more


Process finished with exit code 255
alotia
  • 47
  • 1
  • 7
  • There is no option to provide a customized message when using a `PreAssignmentValidator`. You can, however, use any standard validation library (such as [Hibernate Validator](https://hibernate.org/validator/)), to perform _post-conversion_ validation. Hibernate, for example, supports custom parameterized messages. – andrewJames Feb 20 '21 at 00:53
  • @andrewjames Thanks for your suggestion. I am still struggling to see how to get it to work the way I would like it to function. I added some simple validation to the setter and if the validation fails, throw a CsvValidationException. But it's not doing how I had intended. I will update my sample with what I have done. If you are able to post an example, that would be really helpful. – alotia Feb 21 '21 at 18:20
  • Are you asking for an example based on your approach (which I am not able to do) or for an example based on Hibernate? – andrewJames Feb 21 '21 at 19:37
  • @andrewjames If the hibernate approach can show how to capture the validation errors during parsing, and then list these out at the end of parsing, that would be really helpful! – alotia Feb 22 '21 at 02:32

2 Answers2

2

If you don't want to add another library, you can also create a custom validator to use instead of MustMatchRegexExpression:

public class MySpecialMustMatchRegexExpression implements StringValidator {
    private String regex = "";


    public MustMatchRegexExpression() {
        this.regex = "";
    }

    @Override
    public boolean isValid(String value) {
        if (regex.isEmpty()) {
            return true;
        }
        return value.matches(regex);
    }

    @Override
    public void validate(String value, BeanField field) throws CsvValidationException {
        if (!isValid(value)) {
            throw new CsvValidationException(String.format(ResourceBundle.getBundle("mustMatchRegex", field.getErrorLocale())
                    .getString("validator.regex.mismatch"), field.getDeclaredAnnotations[1], value, regex));
        }
    }

    @Override
    public void setParameterString(String value) {
        if (value != null && !value.isEmpty()) {
            regex = value;
        }
    }
}

And use that in your preassignment validator:

@PreAssignmentValidator(validator = MySpecialMustMatchRegexExpression.class, paramString = "^[A-Za-z]{3,8}$")
@CsvBindByName(column = "Contact First Name", required = true)
private String contactFirstName;
sigma1510
  • 1,165
  • 1
  • 11
  • 26
1

Hibernate's validation is post-conversion - so that means you have to read the data from CSV into the target Contact list of beans first.

But you can still capture all the validation messages in a systematic way - and you can customize them to meet your specific needs.

First you need some extra libraries:

  • Hibernate validator (and its dependencies)
  • Glassfish's EL (expression language) processor

I use Maven for these:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
</dependency>
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.el</artifactId>
    <version>3.0.1-b11</version>
</dependency>

Now you can remove your @PreAssignmentValidator annotations from the Contact class and use this new import:

import javax.validation.constraints.Pattern;

The annotations:

@Pattern(regexp="^[A-Za-z]{3,8}$", 
        message="The value '${validatedValue}' in the 'Contact First Name' column did not match the expected format of 3 to 8 letters.")
@CsvBindByName(column = "Contact First Name", required = true)
private String contactFirstName;

@Pattern(regexp="^[0-9]{10}$", message="Another custom message...")
@CsvBindByName(column = "Phone Number", required = true)
private String phone;

When processing the CSV file, your logic remains the same as in the original question. But instead of using beans.getCapturedExceptions(), you can use Hibernate.

The following imports are needed:

import javax.validation.ValidatorFactory;
import javax.validation.Validator;
import javax.validation.Validation;
import javax.validation.ConstraintViolation;

Hibernate provides the implementation classes you need for these.

The main logic (adapted for my test cases):

Path csvFilePath = Paths.get("C:/tmp/csv/test_01.csv");
final CsvToBean<Contact> beans = new CsvToBeanBuilder<Contact>(
        Files.newBufferedReader(csvFilePath, StandardCharsets.UTF_8))
        .withType(Contact.class)
        .withThrowExceptions(false)
        .build();

List<Contact> contacts = beans.parse();

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

for (Contact contact : contacts) {
    Set<ConstraintViolation<Contact>> violations = validator.validate(contact);
    for (ConstraintViolation<Contact> violation : violations) {
        System.out.println(violation.getMessage());
    }
}

For a CSV input row of this:

joe9,123456789

You will get the following error message printed:

The value 'joe9' in the 'Contact First Name' column did not match the expected format of 3 to 8 letters.

To be clear, this is not happening during CSV parsing, but as a separate step afterwards. This is (in my experience) typically how Hibernate is used, for basic validation.

andrewJames
  • 19,570
  • 8
  • 19
  • 51