What is the elegant way to generate Map from List using streams

894 views Asked by At

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

There are 2 answers

6
Tagir Valeev On BEST ANSWER

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()));
}
7
manish On

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.