0

I was reading the article about validation using Predicates here. I am trying to implement it in Spring Boot framework where I am having some questions.

In the code:

public class LamdaPersonValidator implements PersonValidator {

   public void validate(Person person) {
     notNull.and(between(2, 12)).test(person.getFirstName()).throwIfInvalid("firstname");
     notNull.and(between(4, 30)).test(person.getLastName()).throwIfInvalid("secondname");
     notNull.and(between(3, 50)).and(contains("@")).test(person.getEmail()).throwIfInvalid("email");
     intBetween(0, 110).test(person.getAge()).throwIfInvalid("age");
   }

 }

it is not mentioned on what could be the standard way to check if the person object in the validate method is itself is null. Is it OK to just put a null check like if(persone != null) { // notNull.and..} or there could be some better way to do null check.

Another thing is suppose, I want to do some custom checks like if person exists in the database or not. In this case, I need to connect to the database to check so. In this case, I need to Autowire the interface where static variable and method is not possible.

So, what could be best approach to use this when doing validation from the database?

Bogl
  • 125
  • 2
  • 11
  • Oh yeah, that code looks so much better than ordinary `if` statements. At least, when you fail to use `&&` or extracting common code into methods, like the author of that article. Besides that, why does an e-mail not have more than 50 characters or why are people not allowed to become older than 110? Both constraints have real life contradictions. [And we don’t want to talk about names](https://shinesolutions.com/2018/01/08/falsehoods-programmers-believe-about-names-with-examples/)… – Holger Oct 23 '18 at 09:58

2 Answers2

3

We are not the code judges of the holy inquisition, so it’s not our duty to tell you, whether it is “OK to just put a null check”.

Of course, it is ok to write is as an ordinary if statement, like we did the last 25 years, just like it is ok to invent a verbose framework encapsulating the null check and bringing the term “lambda” somehow into it. The only remaining question would be if you really intent to write if(person != null) { /* do the checks */ }, in other words, allow a null person to pass the test.

In case, you want to reject null persons (which would be more reasonable), there is already a possibility to write it without an explicit test, Objects.requireNonNull, since Java 7, which demonstrates that you don’t need an “everything’s better with lambdas” framework to achieve that goal. Generally, you can write validating code reasonably with conventional code, contrary to the article’s example, utilizing simple tools like the && operator and putting common code into methods:

public void validate(Person person) {
    Objects.requireNonNull(person, "person is null");
    checkString(person.getFirstName(), "first name", 2, 12);
    checkString(person.getLastName(), "last name", 4, 30);
    checkString(person.getEmail(), "email", 3, 50);
    if(!person.getEmail().contains("@"))
        throw new IllegalArgumentException("invalid email format");
    checkBounds(person.getAge(), "age", 0, 110);
}
private void checkString(String nameValue, String nameType, int min, int max) {
    Objects.requireNonNull(nameValue, () -> nameType+" is null");
    checkBounds(nameValue.length(), nameType, min, max);
}
private void checkBounds(int value, String valueType, int min, int max) {
    if(value < min || value > max)
        throw new IllegalArgumentException(valueType+" is not within ["+min+" "+max+']');
}

This does the same as your code, without any framework with “Lambda” in its name, still having readable validation code and allowing to reuse the checking code. That said, instead of a class name LamdaPersonValidator, which reflects how you implemented it, you should use class names reflecting the responsibilities of a class. Clearly, a validator responsible for validating some properties of an object should not get mixed up with a validator checking the presence of an entity in the database. The latter is an entirely different topic on its own and should also be in a question on its own.

The code above is only meant to be an example how to achieve the same as the original code. It should never appear in production code in this form, as it is a demonstration of a widespread anti-pattern, to apply arbitrary unreasonable constraints to properties, most likely invented by the programmer while writing the code.

Why does it assume that a person must have a first name and a last name and why does it assume that a first name must have at least two and at most twelve characters, while the last name must be between four and thirty characters?

It’s actually not even characters, as the association between char units and actual characters is not 1:1.

A must read for every programmer thinking about implementing name validation, is Falsehoods Programmers Believe About Names (With Examples).

Likewise, Wikipedia’s List of the verified oldest people lists one hundred people having an age above 110.

And there is no reason to assume that an email address can’t have more than fifty characters. A true validation of the correct Email pattern may turn out to be something to omit deliberately…

Holger
  • 285,553
  • 42
  • 434
  • 765
0

You can write GenericValidator like that also:

Write AbstractValidator class for common work:

public abstract class AbstractValidator {

    private Map<Predicate, String> validatorMap = new LinkedHashMap<>();
    protected List<String> messages;

    public AbstractValidator() {
        this.messages = new ArrayList<>();
    }

    protected <E> AbstractValidator add(Predicate<E> predicate, String reason) {
        validatorMap.put(predicate, reason);
        return this;
    }

    protected AbstractValidator apply(String fieldName, Object val) {
        AtomicBoolean flag= new AtomicBoolean(true);
        this.validatorMap.forEach((modifier, reason) -> {
            if (flag.get() && !modifier.test(val)) {
                String message = MessageFormat.format("{0} {1}", fieldName, reason);
                messages.add(message);
                flag.set(false);
            }
        });
        this.validatorMap.clear();
        return this;
    }

    public void end(String exceptionStatus) {
        Optional.ofNullable(messages).filter(CollectionUtils::isEmpty)
                .orElseThrow(() -> {
                    RuntimeException ex = new RuntimeException(exceptionStatus, messages);
                    messages.clear();
                    return ex;
                });
    }
}

Write GenericValidator class which will extend the AbstractValidator for your validation implementation:

public class GenericValidator extends AbstractValidator {

    private GenericValidator() {
        super();
    }

    public static GenericValidator of() {
        return new GenericValidator();
    }

    public GenericValidator nonNull() {
        add(Objects::nonNull, "Field value is null");
        return this;
    }

    public GenericValidator notEmpty() {
        add(StringUtils::isNotEmpty, "Field is empty");
        return this;
    }

    public GenericValidator min(int min) {
        add(s -> ((String) s).length() >= min, "Field min size is " + min);
        return this;
    }

    public GenericValidator max(int max) {
        add(s -> ((String) s).length() <= max, "Field max size is " + max);
        return this;
    }

    public GenericValidator notEmptyList() {
        add(CollectionUtils::isNotEmpty, "Field List is null/Empty");
        return this;
    }

    public GenericValidator apply(String fieldName, Object val) {
        return (GenericValidator) super.apply(fieldName, val);
    }
}

Please test accordingly. Example for test cases:

class GenericValidatorTest {

    @Test
    void genericValidationSuccessCase() {
        Abc abc = new Abc();
        abc.setName("a");
        abc.setVal(1);
        abc.setAbslist(Collections.singletonList(new ChildAbc()));
        GenericValidator of = GenericValidator.of();
        of.nonNull().apply("abc", abc).end(GENERIC_JSON_SERIALIZATION);
        of.notEmpty().min(1).max(1).apply("name", abc.getName())
                .nonNull().apply("value", abc.getVal())
                .notEmptyList().apply("childAbc", abc.getAbslist())
                .end(GENERIC_JSON_SERIALIZATION);
    }

    @Test
    void genericValidationWhenObjectNull() {
        GenericValidator of = GenericValidator.of();
        Assertions.assertThrows(BusinessException.class, () -> of.nonNull()
                .apply("abc", null).end(GENERIC_JSON_SERIALIZATION));
    }

    @Test
    void genericValidationWithExceptionInput() {
        Abc abc = new Abc();
        abc.setName("a");
        abc.setVal(1);
        GenericValidator of = GenericValidator.of();
        of.nonNull().apply("abc", abc).end(GENERIC_JSON_SERIALIZATION);
        GenericValidator apply = of.notEmpty().min(1).max(1).apply("name", abc.getName())
                .nonNull().apply("value", abc.getVal())
                .notEmptyList().apply("childAbc", abc.getAbslist());
        Assertions.assertThrows(BusinessException.class, () -> apply.end(GENERIC_JSON_SERIALIZATION));

    }
}

class Abc {

    String name;
    Integer val;
    List<ChildAbc> abslist;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getVal() {
        return val;
    }

    public void setVal(Integer val) {
        this.val = val;
    }

    public List<ChildAbc> getAbslist() {
        return abslist;
    }

    public void setAbslist(List<ChildAbc> abslist) {
        this.abslist = abslist;
    }
}

class ChildAbc {

    String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
fcdt
  • 2,371
  • 5
  • 14
  • 26
Akash Ak
  • 11
  • 2