Search code examples
javajava-stream

Convert List of objects to nested ordered map of ordered lists


I want to convert a list of DTOs into a deep nested map.

Source format:

[
    Dto(validationType=BASIC, source=BASE, address=A4, error=Client is a required field.),
    Dto(validationType=BASIC, source=BASE, address=A4, error=Client must be one of 'I', 'G' or 'W'.),
    Dto(validationType=BASIC, source=BASE, address=B5, error=Individuals require First and Last Names.),
    Dto(validationType=BASIC, source=BASE, address=D6, error=Individuals require First and Last Names.),
    Dto(validationType=BASIC, source=BASE, address=J7, error=First is a required field.),
    Dto(validationType=BASIC, source=BASE, address=L8, error=Last is a required field.),
    Dto(validationType=BASIC, source=BASE, address=M9, error=Name is a required field.),
    Dto(validationType=BASIC, source=BASE, address=N10, error=Status is a required field.),
    Dto(validationType=BASIC, source=BASE, address=N10, error=Status must be one of 'A' or 'P'.),
    Dto(validationType=BASIC, source=BASE, address=F16, error=Groups require a Group Name.)
]

Target format:

{
    BASIC = {
        BASE = {
            A4 = [ "Client is a required field.", "Client must be one of 'I', 'G' or 'W'." ],
            B5 = [ "Individuals require First and Last Names." ],
            D6 = [ "Individuals require First and Last Names." ],
            J7 = [ "First is a required field." ],
            L8 = [ "Last is a required field." ],
            M9 = [ "Name is a required field." ],
            N10 = [ "Status is a required field." ],
            N10 = [ "Status must be one of 'A' or 'P'." ],
            F16 = [ "Groups require a Group Name." ]
        }
    }
}

This code snippet:

final Map<String, Map<String, Map<String, ArrayList<String>>>> collect =
    dtoList.stream()
        .collect(
            Collectors.groupingBy(
                Dto::getValidationType,
                Collectors.groupingBy(
                    Dto::getSource,
                    Collectors.groupingBy(
                        Dto::getAddress,
                        Collectors.mapping(
                            Dto::getError,
                            Collectors.collectingAndThen(
                                Collectors.toSet(),
                                ArrayList::new))))));

produces this map:

{
    BASIC = {
        BASE = {
key       ->            D6 = [ "Individuals require First and Last Names."],
order     ->            B5 = [ "Individuals require First and Last Names.],
not       ->            A4 = [ "Client must be one of 'I', 'G' or 'W'.", "Client is a required field."], <- values' original order lost
preserved ->            F16 = [ "Groups require a Group Name." ],
          ->            N10 = [ "Status must be one of 'A' or 'P'.", "Status is a required field." ],
          ->            M9 = [ "Name is a required field." ],
          ->            L8 = [ "Last is a required field." ],
          ->            J7 = [ "First is a required field." ]
        }
    }
}

The problems are:

  1. The keys under BASE are not sorted. I know I need something like a TreeSet, but I am not sure how to collect into such a collection.
  2. The values within each list are not preserving their order. "Client is a required field." should come before "Client must be one of 'I', 'G' or 'W'.", because that is their original order.

Solution

  • You need to pass in the Supplier mapFactory to specify the type of Map you wish to collect in (()->new TreeMap<>()). Same in case of TreeSet

        final TreeMap<String, TreeMap<String, TreeMap<String, TreeSet<String>>>> collect =
            list.stream()
                .collect(
                    groupingBy(
                        Dto::getValidationType,
                        TreeMap::new,
                        groupingBy(
                            Dto::getSource,
                            TreeMap::new,
                            groupingBy(
                                Dto::getAddress,
                                TreeMap::new,
                                Collectors.mapping(
                                    Dto::getError,
                                    Collectors.collectingAndThen(
                                        Collectors.toSet(),
                                        TreeSet::new)
                                )
                            )
                        )
                    )
                );