1

I have students list that I get from database. I applied predicates to List and partition the List with valid, invalid students. Now for invalid students I want to generate Map with student as a Key and error message as a value. Because I need to generate report for each student.Here what I am doing but I don't know whether it is a good approach or not or is there a better way to do it.

Actually after getting invalid students list I am trying to create a function but i Think code is getting messy and may be there is a better approach to do it. Here what I am doing

private List<Predicate<OlccStudent>> getAllPredicates() {
    List<Predicate<OlccStudent>> allPredicates = Arrays.asList(
            isValidFirstName(),
            isValidMiddleInitial(),
            isValidLastName(),
            isValidStreetAddress(),
            isValidCity(),
            isValidState(),
            isValidZip(),
            isValidDateOfBirth(),
            isValidSsn(),
            isValidTestDate(),
            isValidTestAnswers(),
            isValidProviderCode(),
            isValidInstructorCode()
    );
    return allPredicates;
}

public Map<Boolean, List<OlccStudent>> getStudentsMap(List<OlccStudent> olccStudentsList) {

    List<Predicate<OlccStudent>> allPredicates = getAllPredicates();
    Predicate<OlccStudent> compositePredicate =  allPredicates.stream()
                             .reduce(w -> true, Predicate::and);

    Map<Boolean, List<OlccStudent>> studentsMap= olccStudentsList
                .stream()
                .collect(Collectors.partitioningBy(compositePredicate));

    return studentsMap;
}

public Map<OlccStudent, String> getInvalidStudentsMap(Map<Boolean, List<OlccStudent>> studentsMap) throws Exception {

    List<OlccStudent> invalidStudentsList = 
            studentsMap.entrySet()
             .stream()
             .filter(p -> p.getKey() == Boolean.FALSE)
             .flatMap(p -> p.getValue().stream())
             .collect(Collectors.toList());

    Function<List<OlccStudent>, Map<OlccStudent, String>> invalidStudentFunction = list ->  {

        Map<OlccStudent, String> invalidStudentsMap = new LinkedHashMap<>();

        list.forEach(student-> {
            String errorMessage = getStudentErrorMessage(student);
            invalidStudentsMap.put(student, errorMessage);  
        });

        return invalidStudentsMap;
    };

    Map<OlccStudent, String> invalidStudentsMap = invalidStudentFunction.apply(invalidStudentsList);
    return invalidStudentsMap;

    return null;
}

private String getStudentErrorMessage(OlccStudent student) {

    String firstName = student.getFirstName();
    String middleInitial = student.getMiddleInitial();
    String lastName = student.getLastName();
    String streetAddress = student.getStreetAddress();
    ....

    StringBuilder errorBuilder = new StringBuilder();

    //The predicate 'negate' method returns a predicate that represents the logical negation or opposite
    if (isValidFirstName().negate().test(student)) {
        String error = "Invalid FirstName: " + firstName;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidMiddleInitial().negate().test(student)) {
        String error = "Invalid Middle Initial: " + middleInitial;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidLastName().negate().test(student)) {
        String error = "Invalid LastName: " + lastName;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidStreetAddress().negate().test(student)) {
        String error = "Invalid StreetAddress: " + streetAddress;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }
     ...

    if (errorBuilder.length() > 0) {
        errorBuilder.deleteCharAt(errorBuilder.length() - 1);
    } 

    return errorBuilder.toString().trim();
}

Actually I am confuse with the getStudentErrorMessage() that I am calling from list.forEach(). I know collectors provide you Collectors.joining function. Actually I want to do it in a Predicate manner. Like I created list of all predicates and then use it in the streams. Can I do similar thing with my error messages? Like I create stream from my invalid students List, and using Collectors.toMap() , put my Student as a Key and error message as its value.

Thanks

Edit ----------------

public class OlccStudentPredicate {

    public static Predicate<OlccStudent> isValidTestDate() {
        return p -> isValidDate(p.getTestDate());
    }

    public static Predicate<OlccStudent> isValidDateOfBirth() {
        return p -> isValidDate(p.getDateOfBirth());
    }

    ...

    private static boolean isValidDate(String date) {
    boolean valid = false;
    if (StringUtils.isNotBlank(date)) {
        try {
            LocalDate.parse(date, DateTimeFormatter.ofPattern("MM/dd/yyyy"));
            valid = true;
        } catch (DateTimeParseException e) {

        }
    } 
    return valid;
}


@Test
public void test() {

    List<OlccStudent> olccStudentsList = getOlccStudentsList();

    try {

        Map<OlccStudent, String> map = getInvalidStudentsMap(olccStudentsList);
        System.out.println();
        //olccStudentService.getStudentsMap(olccStudentsList);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

private List<OlccStudent> getOlccStudentsList() {

    List<OlccStudent> olccStudentsList = new ArrayList<>();

    OlccStudent student1 = new OlccStudent(1L, 1L, "firstName1", "middleInitial1", "lastName1", "email1", 
            "streetAdress1", "city1", "state1", "1234", "01/22/2015", "phoneNumber1", "01/22/2015", 
            "123456789", "testForm1", "providerCode1", "instructorCode1", "surveyAnswer1", 
            "testIdentifier1",  "testAnswers1");

    OlccStudent student2 = new OlccStudent(2L, 2L, "firstName2", "middleInitial2", "lastName2", "email2", 
            "streetAdress2", "city2", "state2", "5678", "02/22/2015", "phoneNumber2", "02/22/2015", 
            "987654321", "testForm2", "providerCode2", "instructorCode2", "surveyAnswer2", 
            "testIdentifier2",  "testAnswers2");

    OlccStudent student3 = new OlccStudent(3L,3L, "firstName3", "middleInitial3", "lastName3", "email3", 
            "streetAdress3", "city3", "state3", "zip3", "testDate3", "phoneNumber3", "dateOfBirth3", 
            "socialSecurityNumber3", "testForm3", "providerCode3", "instructorCode3", "surveyAnswer3", 
            "testIdentifier3",  "testAnswers3");

    OlccStudent student4 = new OlccStudent(4L, 4L, "firstName4", "middleInitial4", "lastName4", "email4", 
            "streetAdress4", "city4", "state4", "zip4", "testDate4", "phoneNumber4", "dateOfBirth4", 
            "socialSecurityNumber4", "testForm4", "providerCode4", "instructorCode4", "surveyAnswer4", 
            "testIdentifier4",  "testAnswers4");

    olccStudentsList.add(student1);
    olccStudentsList.add(student2);
    olccStudentsList.add(student3);
    olccStudentsList.add(student4);

    return olccStudentsList;
}

private String validate(OlccStudent student) {

    String firstName = student.getFirstName();
    String middleInitial = student.getMiddleInitial();
    String lastName = student.getLastName();
    String streetAddress = student.getStreetAddress();
    String city = student.getCity();
    String state = student.getState();
    String zip = student.getZip();
    String dateOfBirth = student.getDateOfBirth();
    String ssn = student.getSocialSecurityNumber();
    String phoneNumber = student.getPhoneNumber();
    String testDate = student.getTestDate();
    String testForm = student.getTestForm();
    String testAnswers = student.getTestAnswers();
    String providerCode = student.getProviderCode();
    String instructorCode = student.getInstructorCode();

    StringBuilder errorBuilder = new StringBuilder();

    //The predicate 'negate' method returns a predicate that represents the logical negation or opposite
    if (isValidFirstName().negate().test(student)) {
        String error = "Invalid FirstName: " + firstName;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidMiddleInitial().negate().test(student)) {
        String error = "Invalid Middle Initial: " + middleInitial;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidLastName().negate().test(student)) {
        String error = "Invalid LastName: " + lastName;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidStreetAddress().negate().test(student)) {
        String error = "Invalid StreetAddress: " + streetAddress;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidCity().negate().test(student)) {
        String error = "Invalid City: " + city;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidState().negate().test(student)) {
        String error = "Invalid State: " + state;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidZip().negate().test(student)) {
        String error = "Invalid Zip: " + zip;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidDateOfBirth().negate().test(student)) {
        String error = "Invalid DateOfBirth: " + dateOfBirth;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidSsn().negate().test(student)) {
        String error = "Invalid SSN: " + ssn;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidTestDate().negate().test(student)) {
        String error = "Invalid TestDate: " + testDate;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidTestAnswers().negate().test(student)) {
        String error = "Invalid TestAnswers: " + testAnswers;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidProviderCode().negate().test(student)) {
        String error = "Invalid ProvideCode: " + providerCode;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (isValidInstructorCode().negate().test(student)) {
        String error = "Invalid InstructorCode: " + instructorCode;
        errorBuilder.append(error + ERROR_MESSAGE_SEPERATOR);
    }

    if (errorBuilder.length() > 0) {
        errorBuilder.deleteCharAt(errorBuilder.length() - 1);
    } 

    return errorBuilder.toString().trim();
}

public Map<OlccStudent, String> getInvalidStudentsMap(List<OlccStudent> studentsList) throws Exception {

    Map<OlccStudent, String> map = studentsList.stream()
      // Step 1: Validate each student, keeping a track of any error message generated.
      .collect(Collectors.toMap(Function.identity(), student -> validate(student)))
      // Step 2: Keep only those that have an error message associated.
      .entrySet()
      .stream()
      .filter(entry -> entry.getValue() != null)
      // Step 3: Generate a Map.
      .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()));

    return map;

}

2 Answers 2

3

I would create a ValidationRule class to stick together validation predicate and error message formatter:

static class ValidationRule {
    public final Predicate<OlccStudent> predicate;
    public final Function<OlccStudent, String> errorFormatter;

    public ValidationRule(Predicate<OlccStudent> predicate,
            Function<OlccStudent, String> errorFormatter) {
        this.predicate = predicate;
        this.errorFormatter = errorFormatter;
    }
}

Now getAllRules will look like this:

public static List<ValidationRule> getAllRules() {
    return Arrays.asList(
        new ValidationRule(isValidFirstName(), s -> "Invalid FirstName: " + s.getFirstName()),
        new ValidationRule(isValidMiddleInitial(), s -> "Invalid Middle Initial: " + s.getMiddleInitial()),
        new ValidationRule(isValidLastName(), s -> "Invalid LastName: " + s.getLastName()),
        new ValidationRule(isValidStreetAddress(), s -> "Invalid StreetAddress: " + s.getStreetAddress())
        // ...
        );
}

And you can get the map of invalid students in the following way:

public Map<OlccStudent, String> getInvalidStudentsMap(List<OlccStudent> students) {
    List<ValidationRule> rules = getAllRules();
    return students
            .stream()
            .<Entry<OlccStudent, String>>map(student -> new AbstractMap.SimpleEntry<>(student, rules
                    .stream()
                    .filter(rule -> rule.predicate.test(student))
                    .map(rule -> rule.errorFormatter.apply(student))
                    .collect(Collectors.joining(ERROR_MESSAGE_SEPERATOR))))
            .filter(entry -> !entry.getValue().isEmpty())
            .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()));
}
6
  • @Tiger Valeev Thanks. I tried your code and I am getting error that Type mismatch: cannot convert from Map<Object,Object> to Map<OlccStudent,String>
    – Basit
    Commented Jun 10, 2015 at 18:17
  • 1
    @Basit: probably you have older compiler version which cannot infer type arguments. I added explicit arguments to map, try now. Commented Jun 11, 2015 at 2:18
  • I agree with this suggestion as it makes the code more object-oriented and allows adding more rules in the future easier.
    – manish
    Commented Jun 11, 2015 at 5:21
  • In the chaining call case, there is also a trick to use the anonymous type. java students.stream() .map(student -> new Object() { OlccStudent student = student; String rule = rules......; }) .filter(x -> !x.rule.isEmpty()) // ......
    – JasonMing
    Commented Apr 19, 2017 at 8:06
  • 1
    @TagirValeev yes, absolutely, that is really work for me. So, I call it a trick. In that case, the generic parameter <R> of map(...) is not only an Object, it is a concrete anonymous type (e.g. ValidationRule$0 ). So, you can access the member rule of the anonymous type in the continuation call, but you cannot assign it to any variable. Because you cannot declare the concrete type as the variable type, you can only declare the variable as an Object, then you can not access the members anymore.
    – JasonMing
    Commented Apr 20, 2017 at 9:59
0

Part 1: Generating a Map from a Collection of objects


If I understand your question correctly, you want to generate a Map from a Collection of objects such that the values in the Map are error messages generated while validating objects in the Collection and the keys are the actual objects that failed validation(s).

So, if your input Collection is:

Student 1: Valid
Student 2: Valid
Student 3: Invalid
Student 4: Valid
Student 5: Invalid

The expected output is:

Student 3: {error message}
Student 5: {error message}

If this is what you want, your question is overly complicated and can be simplified by assuming that there exists a function such as:

/**
 * Validates a student and returns an error message if the student data is
 * not valid.  The error message provides the actual reason why the student
 * data is invalid.
 *
 * @param student The student to validate.
 * @return An error message if {@code student} contains invalid data,
 *         {@code null} otherwise.
 */
String validate(OlccStudent student) { ... }

Now, the task is straightforward.

// Step 0: We have a collection of students as an input.
Collection<OlccStudent> students = ...;

students.stream()
  // Step 1: Validate each student, keeping a track of any error message generated.
  .collect(Collectors.toMap(Function.identity(), student -> validate(student)))
  // Step 2: Keep only those that have an error message associated.
  .entrySet()
  .stream()
  .filter(entry -> entry.getValue() != null);
  // Step 3: Generate a Map.
  .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()));

Part 2: Validating objects


It may be better to use the JSR303 - Java Validation API (and one of its implementations such as Hibernate Validator or Apache Beans Validator) to validate beans. This is not only standardized but requires less effort and less maintenance. Adding new validations is easy and the whole framework is locale-agnostic, allowing generation of locale-specific messages.

7
  • Thanks for your answer. Your comments are very helpful But I have already partitioned my List in to valid and invalid. Actually I partitioned first because I Have to create map for only invalid students. Valid students need no map. I tried this Map<OlccStudent, String> invalidStudentsMap = invalidStudentsList .stream() .collect(Collectors.toMap(p -> p, (p) -> getStudentErrorMessage(p)));
    – Basit
    Commented Jun 10, 2015 at 17:41
  • The end result of my sample is a Map that contains only invalid students. Have you tried it?
    – manish
    Commented Jun 10, 2015 at 17:42
  • if I use getStudentErrorMessage(p) in your code. In your code is validate(student). So it doesn't matter. Then I am getting all 4 records with 2 valid records have no value, and two invalid records with error value
    – Basit
    Commented Jun 10, 2015 at 17:54
  • I have run my code and it works exactly as I have described. First take it verbatim and run it to make sure it does what is advertises. Then, if you want to rename validate to getStudentErrorMessage, that is fine because that is just a renaming of a method. I think you are sticking to your original code and trying to plug in parts of the sample I have provided and concluding it does not work. Otherwise, post a sample on Gist with exactly my code that demonstrates any problems. What I have provided to you has been taken from my own production app so I know it works in real life too.
    – manish
    Commented Jun 10, 2015 at 18:00
  • I edited my post to add a test sample of your code. Kindly check. I get 4 entires in map. I should get 2. student 1 and 2 are valid because they contain valid dates, while 3 and 4 are invalid because they contain invalid date format. Thanks
    – Basit
    Commented Jun 10, 2015 at 18:12

Not the answer you're looking for? Browse other questions tagged or ask your own question.