Search code examples
javaspringjava-stream

Form a collection structure from flat object list using Java stream api


I've following flat list of objects:

public class FlatQuestion{
   public int QuestionId;
   public String QuestionName;
   public int SectionId;
   public String SectionName;
   public int FieldId;
   public String FieldName;
   public int Input_Type_Id;
   public String Input_Type_String;
}

I've been trying to convert it the list of questions. List<Question>

public class Question {

    public int id;
    public String name;
    public List<Section> sections;

    Question(String name, int id, List<Section> sections) {
        this.name = name;
        this.id = id;
        this.sections = sections;
    }
}

public class Section {
    
    public int id;
    public String name;
    public List<Field> fields;

    Section(int id, String name, List<Field> fields) {
        this.name = name;
        this.id = id;
    this.fields = fields;
    }

}

public class Field {

    public int id;
    public String lable;
    public Input input_type;

    Field(int id, String lable, Input input_type) {
    this.id = id;
    this.lable = lable;
        this.input_type = input_type;
    }
}

public class Input {

    public int id;
    public String type;

    Input(int id, String type) {
    this.id = id;
    this.type = type;
    }
}

The solution I've been working on is giving me multiple sections. How to get distinct values here:

final List<Question> results = data
    .stream()
    // extracting distinct questions, you have fetched  
    .map(FlatQuestion::QuestionId)
    .distinct()
    // now we have unique key for the data and can map it
    .map(it -> 
         new Question(
             it, 
             // extracting all sections, which  were fetched in rows with references to templates.
             data
                 .stream()
                 .filter(o -> o.QuestionId.equals(it))
                 .map(ing -> new Sections(ing.SectionId, ing.SectionName))
                 .collect(Collectors.toSet())
    .collect(Collectors.toList()) ;

Adding some sample data:

FlatQuestion f1 = new FlatQuestion(1, "Student Survey", 1, "Basic info", 1, "Enter Name", 1, "Text");
FlatQuestion f2 = new FlatQuestion(1, "Student Survey", 1, "Basic info", 2, "Enter Address", 1, "Text");
FlatQuestion f3 = new FlatQuestion(1, "Student Survey", 2, "Class Info", 3, "Select No of subjects", 2, "Dropdown");
FlatQuestion f4 = new FlatQuestion(1, "Student Survey", 2, "Class info", 4, "Enter primary subject", 1, "Text");
FlatQuestion f5 = new FlatQuestion(2, "Patient Audit", 3, "Basic info", 5, "Enter Name", 1, "Text");
FlatQuestion f6 = new FlatQuestion(2, "Patient Audit", 3, "Basic info", 6, "Enter Address", 1, "Text");
FlatQuestion f7 = new FlatQuestion(2, "Patient Audit", 4, "Alcohol Consumption", 7, "How often you drink", 3, "Checkbox");

Solution

  • Let's take it step by step. Firstly, we'll need a method to map/create a Question from a FlatQeustion (assuming there are no duplicates):

    private Question mapQuestion(FlatQuestion flatQuestion) {
        return new Question(
                flatQuestion.getQuestionId(),
                flatQuestion.getQuestionName(),
                List.of(new Section(
                        flatQuestion.getSectionId(),
                        flatQuestion.getSectionName(),
                        List.of(new Field(
                                flatQuestion.getFieldId(),
                                flatQuestion.getFieldName(),
                                new Input(
                                        flatQuestion.getInput_Type_Id(),
                                        flatQuestion.getInput_Type_String()
                                ))
                        ))
                ));
    }
    

    Then, for duplicates, let's start from the bottom. When we'll merge 2 Question objects, we'll need to merge their list of Sections, avoiding duplicates, so we need a method to merge the two List avoiding duplicates. it can be something like this:

    private List<Section> mergeSections(List<Section> sections, List<Section> otherSections) {
            // we'll group sections by section id and merge the duplicates with the merge function
            Map<Integer, Section> allSectionsById = Stream.concat(sections.stream(), otherSections.stream())
                    .collect(Collectors.toMap(Section::getId, s -> s, this::mergeSectionsWithSameId));
            // the map has no duplicates now, we can safely return the values as a list
            return new ArrayList<>(allSectionsById.values());
    }
    

    As you can see, Collectors.toMap allows us to specify a mapper for the key (in our case Section::getId, a mapper for the value, and a mergeFunction). this merge function will be called when we have 2 elements with the same key. In our case we'll use these 2 Sections with the same key to create a merged one with all the fields:

    private Section mergeSectionsWithSameId(Section first, Section second) {
        if (first.getId() != second.getId()) {
            throw new IllegalArgumentException("cannot merge questions with different ids");
        }
        List<Field> allFields = new ArrayList<>();
        allFields.addAll(first.getFields());
        allFields.addAll(second.getFields());
        return new Section(first.getId(), first.getName(), allFields);
    }
    

    This means we can move one level up and use this functions to merge two Questions with the same id:

    private Question mergeQuestionsWithSameId(Question first, Question second) {
        if (first.getId() != second.getId()) {
            throw new IllegalArgumentException("cannot merge questions with different ids");
        }
        return new Question(
                first.getId(),
                first.getName(),
                mergeSections(first.getSections(), second.getSections())
        );
    }
    

    Finally, let's apply the same technique for the Stream of Question objects:

    Map<Integer, Question> questionById = Stream.of(f1, f2, f3, f4, f5, f6, f7)
        .collect(Collectors.toMap( FlatQuestion::getQuestionId, 
                                   this::mapQuestion, 
                                   this::mergeQuestionsWithSameId )
    );
    

    As you can see, we are using the same Colllectors.toMap and we group the elements by questionId. Additionally, instead of keeping the elements as they are (previously q -> q) now we map them from FlatQuestion to Question (with q -> mapQuestion(q) or simply this::mapQuestion).

    Finally, we deal with the questions with the same id using (q1, a2) -> mergeQuestionsWithSameId(q1, q2) or simply this::mergeQuestionsWithSameId. You can access the resulting collection of unique questions with no duplicated fields using questionById.vales()