7

I have a question regarding validating data in a nested for loop.

public class Object1{
  private String obj1Name;

  private String obj1Desc;

  private List<Object2> object2List;

  //Setters and getters
}


public class Object2{
  private String obj2Name;

  private String obj2Desc;

  private List<Object3> object3List;

  //Setters and getters
}

public class Object3{
  private String obj3Name;

  private String obj3Desc;
  //Setters and getters
}

I wish to validate both name and desc in all objects, instead of using a nested loop like the following:

List<Object1> object1List = getObject1List();

for(Object1 object1 : object1List ){
   if(object1.getObj1Name() == null){
     //throw error
   }

   if(object1.getObj1Desc() == null){
     //throw error
   }

   for(Object2 object2 : object1.getObject2List()){
        if(object2.getObj2Name() == null){
            //throw error
        }

        if(object2.getObj2Desc() == null){
            //throw error
        }

        //loop Object 3 ...
   }
}

Is there any better way to do it?

Boann
  • 48,794
  • 16
  • 117
  • 146
hades
  • 4,294
  • 9
  • 46
  • 71

7 Answers7

4

I am just going to say it here, so that no one does what you want to do - use a proper framework for this, personally I would go for Hibernate Validator - super easy to integrate and use IMO. It will support that nesting that you have without any problems and there are tons of tutorials online (even their own is good) and how to achieve that; hint: a few dependencies and a few annotations and you are done.

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • yeah, i actually know about hibernate validator, normally i use it to validate object request in `@RequestBody`. – hades Dec 05 '18 at 13:43
  • Thanks for the reminder, it don't come into my mind because i thought there is no way triggering the validation process, but it actually has. – hades Dec 05 '18 at 14:20
3

EDIT: An idea to externalize the check, you need to create Functional Interface

ObjectValidatorInterface ov = new ObjectValidator();
if(!object1List.stream().allMatch(o -> ov.validate(o, Object1.class))) {
        // throw error;
}

And the interface

@FunctionalInterface
interface ObjectValidatorInterface {
    boolean validate(Object object, Class clazz);
}

class ObjectValidator implements ObjectValidatorInterface {

    public boolean validate(Object object, Class clazz) {
        boolean valid = false;

        if(Object1.class.getName().equals(clazz.getName())) {
            valid = validateObject1((Object1) object);

        } else if(Object2.class.getName().equals(clazz.getName())) {
            valid = validateObject2((Object2) object);

        } else if(Object3.class.getName().equals(clazz.getName())) {
            valid = validateObject3((Object3) object);
        }

        return valid;
    }

    private boolean validateObject1(Object1 o) {
        boolean valid;
        valid = o.getObj1Name() != null && o.getObj1Desc() != null;
        if(!(o.getObject2List() == null || o.getObject2List().isEmpty())) {
            valid = valid && o.getObject2List().stream().allMatch(o2 -> validate(o2, Object2.class));
        }
        return valid;
    }

    private boolean validateObject2(Object2 o) {
        boolean valid;
        valid = o.getObj2Name() != null && o.getObj2Desc() != null;
        if(!(o.getObject3List() == null || o.getObject3List().isEmpty())) {
            valid = valid && o.getObject3List().stream().allMatch(o3 -> validate(o3, Object3.class));
        }
        return valid;
    }

    private boolean validateObject3(Object3 o) {
        return o.getObj3Name() != null && o.getObj3Desc() != null;
    }
}

ORIGINAL ANSWER

You might be able to do it by delegating the validation to each object:

List<Object1> object1List = getObject1List();

if(!object1List.stream().allMatch(Object1::isValid)) {
    //throw error
}

And add an isValid method to each object

public class Object1 {
    private String obj1Name;
    private String obj1Desc;
    private List<Object2> object2List;

    public boolean isValid() {
        return obj1Name != null
            && obj1Desc != null
            && object2List.stream().allMatch(Object2::isValid);
    }
}

public class Object2 {
    private String obj2Name;
    private String obj2Desc;
    private List<Object3> object3List;

    public boolean isValid() {
        return obj2Name != null
            && obj2Desc != null
            && object3List.stream().allMatch(Object3::isValid);
    }
}

public class Object3 {
    private String obj3Name;
    private String obj3Desc;

    public boolean isValid() {
        return obj3Name != null
            && obj3Desc != null;
    }
}
Bentaye
  • 9,403
  • 5
  • 32
  • 45
  • 3
    I really dislike this - you put the validation conditions inside the POJO itself - is this ever a good idea? what if there are 100 of these? – Eugene Dec 05 '18 at 13:24
  • good one, but I think these `isValid` methods should be moved out of the POJO classes – Andrew Tobilko Dec 05 '18 at 13:24
  • @Bentaye nope, the field names are different - this would not work – Eugene Dec 05 '18 at 13:27
  • @Eugene Added an other version where the check is done by an external object – Bentaye Dec 05 '18 at 14:22
  • @Bentaye well that is a bit better, I agree. But now, imagine supporting that and testing it correctly, with at least 10 different validation types and rules... you might be on the verge of the next mission impossible movie, which I would not want to be part of – Eugene Dec 05 '18 at 14:23
  • @Eugene yes sure, but the OP does not specify how many classes he needs to validate, so assuming 3 as in his example – Bentaye Dec 05 '18 at 14:25
1

Well, you can definitely avoid the "nesting" by using the Stream API:

if(object1List.stream()
                .anyMatch(a -> a.getObj1Name() == null ||
                        a.getObj1Desc() == null)){
    // throw error
}else if(object1List.stream()
                .anyMatch(a -> a.getObject2List().stream()
                       .anyMatch(b -> b.getObj2Name() == null ||
                                            b.getObj2Desc() == null))){
    // throw error
}else if(object1List.stream()
                .anyMatch(a -> a.getObject2List().stream()
                        .anyMatch(b -> b.getObject3List().stream()
                                .anyMatch(c -> c.getObj3Name() == null ||
                                                      c.getObj3Desc() == null)))){
     // throw error
}

Another approach being more compact, but probably less efficient:

boolean result = object1List.stream()
                .flatMap(a -> a.getObject2List().stream()
                        .flatMap(b -> b.getObject3List().stream()
                                .flatMap(c -> Stream.of(a.getObj1Name(),
                                        a.getObj1Desc(), b.getObj2Name(),
                                        b.getObj2Desc(), c.getObj3Name(), c.getObj3Desc()))))
                .anyMatch(Objects::isNull); 

if(result){ // throw error }

So, to conclude if performance is a concern then proceed with your approach or try and see if the parallel stream API can do you any good, otherwise, the above should suffice.

Ousmane D.
  • 54,915
  • 8
  • 91
  • 126
  • 6
    to me, it's way worse than OP's approach – Andrew Tobilko Dec 05 '18 at 13:12
  • 1
    @AndrewTobilko the goal was to remove the nesting, and even in terms of efficiency that really depends on the number of elements in the source. – Ousmane D. Dec 05 '18 at 13:14
  • 2
    Yeah, [flatmap that shit](https://stackoverflow.com/questions/8559537/where-does-the-flatmap-that-s-idiomatic-expression-in-scala-come-from). – michid Dec 05 '18 at 13:24
1

A validator, as a functional interface, is a Consumer which consumes a value of a specific type, performs checks and throws an Exception if something is off.

Traversal of the data structure (Tree) can be accomplished over streams (peek to visit a node, flatmap to recurse into the children). For the validation, we introduce a NO-OP map operation, which validates and returns the value, allowing the stream to continue.

BiConsumer<String, Object> checkRequired = (name, value) -> {
    if (value == null) {
        throw new IllegalArgumentException(name + " is required");
    }
};

Consumer<Object1> obj1Validator = obj1 -> {
    checkRequired.accept("Object1", obj1);
    checkRequired.accept("obj1Name", obj1.getObj1Name());
    checkRequired.accept("obj1Desc", obj1.getObj1Desc());
};

Consumer<Object2> obj2Validator = obj2 -> {
    checkRequired.accept("Object2", obj2);
    checkRequired.accept("obj2Name", obj2.getObj2Name());
    checkRequired.accept("obj2Desc", obj2.getObj2Desc());
};

Consumer<Object3> obj3Validator = obj3 -> {
    checkRequired.accept("Object3", obj3);
    checkRequired.accept("obj3Name", obj3.getObj3Name());
    checkRequired.accept("obj3Desc", obj3.getObj3Desc());
};

Object1 obj1 = ...; // assign some value

Stream.of(obj1)
    .peek(obj1Validator)
    .filter(x -> x.getObject2List() != null)
    .map(Object1::getObject2List)
    .filter(Objects::nonNull)
    .flatMap(List::stream)
    .peek(obj2Validator)
    .map(Object2::getObject3List)
    .filter(Objects::nonNull)
    .flatMap(List::stream)
    .peek(obj3Validator)
    .count();
Peter Walser
  • 15,208
  • 4
  • 51
  • 78
  • now imagine supporting this for numerous rules and fields... seriously, don't. – Eugene Dec 05 '18 at 13:40
  • @Eugene that's what validation is about - check each model and each field. Of course that validation can be delegated to a framework, such as using Bean Validation (JSR-380) which drives field validations over annotations. – Peter Walser Dec 05 '18 at 13:47
  • would you write this code or use a framework? that was my point – Eugene Dec 05 '18 at 13:48
  • I would certainly use a framework, in particular Bean Validation (JSR-380, https://beanvalidation.org/specification) – Peter Walser Dec 05 '18 at 14:07
0

Well, you could use a function/loop which sifts through an object(The generic Object). That way, you won't have to run a separate for loop for each object if the name of the member variable "desc" is uniform in all the three objects. But that's another level of nesting so not sure you'd want to use that if you're just looking to avoid nesting.

benignfoppery
  • 55
  • 1
  • 7
0

If their validation seems to be the same, only the type change, then use interface have Object1, Object2 and Object3 implements ICommon:

interface ICommon {
  String getField1();
  int getField2();
  Iterable<? extends ICommon> getChildren();
}

Then have this:

private void validate0(String prefix, ICommon common) {
  if (common.getField1() == null) throw new ValidationException(prefix + ".field1 can't be null");
  if (common.getField2() == null) throw new ValidationException(prefix + ".field2 can't be null");
  int index = 0;
  for (ICommon child : getChildren()) {
     validate0(prefix + ".children[" + index + "]", child);
    ++index;
  }
}

public void validate(ICommon common) {
   validate0("common", common);
}

You may also invert this using a BiConsumer<String, ICommon> (or Consumer<ICommon>) to validate if you don't care about proper error reporting).

  • This will also make it clearer about which validation may apply on your object
  • Using stream does not (always) make it clearer (see Peter Walser answer and try to understand the big stream :)).
NoDataFound
  • 11,381
  • 33
  • 59
0

Make use of ConstraintViolation and javax annotations validation using javax

public class Object1{
@NotNull
private String obj1Name;
@NotNull
private String obj1Desc;
@NotNull
private List<Object2> object2List;
}


public class Object2{
@NotNull
private String obj2Name;
@NotNull
private String obj2Desc;

}

public Object1 validate() throws someValidationException {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

Object1 object1 = new Object1();
    provision = new ObjectMapper().readValue(rawJson, Object1.class);

    Set<ConstraintViolation<Object1>> constraintViolations =
            validator.validate( object1 );
    if(constraintViolations.size()>1) {
 //throw some exception when object 2 is null for example and some constraint is 
//violated 
}
     return object1;
}