W dzisiejszym wpisie chciałbym przedstawić Ci różne przypadki wykorzystania biblioteki MapStruct w Twoim kodzie. Nie będą to jakieś skomplikowane rozwiązania, jednak dzięki nim dowiemy się co jeszcze potrafi to z pozoru proste narzędzie. W tym wpisie sprawdzimy w jaki sposób MapStruct radzi sobie z zagnieżdżonymi strukturami, jak można wykorzystać jeden mapper w drugim oraz jak mapować wartości jednego enuma do drugiego.

Bardziej skomplikowane struktury

Załóżmy, że chcemy przekonwertować strukturę, która agreguje w sobie inne struktury w postaci klas czy też kolekcji. Stwórzmy, więc odpowiednie elementy naszej układanki - klasy podlegające konwersji, interfejs podstawowego mapper oraz test weryfikujący jego działanie.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package pl.devcezz.mapstruct.tips;

import java.util.Collections;
import java.util.List;

public class Country {

  private final String name;
  private final List<Province> provinces;

  public Country(String name, List<Province> provinces) {
    this.name = name;
    this.provinces = provinces;
  }

  //... getters
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package pl.devcezz.mapstruct.tips;

import java.util.Collections;
import java.util.List;

public class Province {

  private final String name;
  private final List<City> cities;

  public Province(String name, List<City> cities) {
    this.name = name;
    this.cities = cities;
  }

  //... getters
}
1
2
3
4
5
6
7
8
9
10
11
12
package pl.devcezz.mapstruct.tips;

public class City {

  private final String name;

  public City(String name) {
    this.name = name;
  }

  //... getters
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package pl.devcezz.mapstruct.tips;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface CountryMapper {

  CountryMapper INSTANCE = Mappers.getMapper(CountryMapper.class);

  CountryDto map(Country country);

}

Oczywiście dla klas Country, Province oraz City tworzymy ich odpowiednik w postaci DTO. W ramach mappera dodajemy tylko jedną metodę mapującą Country na CountryDto. Jak się zaraz okaże jest to wystarczająca ilość kodu w tym interfejsie. Pozostaje nam napisać test weryfikujący mapowanie, który po uruchomieniu przejdzie bez najmniejszych problemów. MapStruct wygenerował za nas dodatkowe metody w mapperze, które konwertują Province i City na klasy z sufiksem *Dto.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package pl.devcezz.mapstruct.tips;

import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;

public class CountryMapperTest {

  @Test
  void should_map_country_to_dto() {
    Country poland = new Country(
        "Polska",
        List.of(
            createProvince("mazowieckie",
                "Warszawa", "Płock", "Legionowo"
            ),
            createProvince("małopolskie",
                "Kraków"
            ),
            createProvince("pomorskie",
                "Gdańsk", "Gdynia"
            )
        )
    );

    CountryDto result = CountryMapper.INSTANCE.map(poland);

    assertThat(result.getName()).isEqualTo("Polska");
    assertThat(extractAllProvinceNames(result)).containsExactlyInAnyOrder(
        "mazowieckie", "małopolskie", "pomorskie"
    );
    assertThat(extractAllCityNames(result)).containsExactlyInAnyOrder(
        "Warszawa", "Płock", "Legionowo",
        "Kraków",
        "Gdańsk", "Gdynia"
    );
  }

  //... helper methods
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package pl.devcezz.mapstruct.tips;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class CountryMapperImpl implements CountryMapper {
  public CountryMapperImpl() {
  }

  public CountryDto map(Country country) {
    if (country == null) {
      return null;
    } else {
      List<ProvinceDto> provinces = null;
      String name = null;
      provinces = this.provinceListToProvinceDtoList(country.getProvinces());
      name = country.getName();
      CountryDto countryDto = new CountryDto(name, provinces);
      return countryDto;
    }
  }

  protected CityDto cityToCityDto(City city) {
    if (city == null) {
      return null;
    } else {
      String name = null;
      name = city.getName();
      CityDto cityDto = new CityDto(name);
      return cityDto;
    }
  }

  protected List<CityDto> cityListToCityDtoList(List<City> list) {
    if (list == null) {
      return null;
    } else {
      List<CityDto> list1 = new ArrayList(list.size());
      Iterator var3 = list.iterator();

      while(var3.hasNext()) {
        City city = (City)var3.next();
        list1.add(this.cityToCityDto(city));
      }

      return list1;
    }
  }

  protected ProvinceDto provinceToProvinceDto(Province province) {
    if (province == null) {
      return null;
    } else {
      List<CityDto> cities = null;
      String name = null;
      cities = this.cityListToCityDtoList(province.getCities());
      name = province.getName();
      ProvinceDto provinceDto = new ProvinceDto(name, cities);
      return provinceDto;
    }
  }

  protected List<ProvinceDto> provinceListToProvinceDtoList(List<Province> list) {
    if (list == null) {
      return null;
    } else {
      List<ProvinceDto> list1 = new ArrayList(list.size());
      Iterator var3 = list.iterator();

      while(var3.hasNext()) {
        Province province = (Province)var3.next();
        list1.add(this.provinceToProvinceDto(province));
      }

      return list1;
    }
  }
}

Wychodzi na to, że biblioteka znając wewnętrzną strukturę klasy, dla której zdefiniujemy metodę mapującą, sama stworzy wymagane metody konwertujące dla agregowanych struktur. Oczywiście, aby to rozwiązanie zadziałało prawidłowo trzeba upewnić się, że nazwy i typy pól są takie same. W innym przypadku musimy użyć wcześniej przedstawionych adnotacji @Mappings oraz @Mapping na własnoręcznie utworzonych sygnaturach metod dla zawieranych klas. Trzeba jednak uważać na jedną rzecz. Jeśli po wygenerowaniu mappera zmienimy nazwę dla jednej z agregowanych struktur, dla których explicite nie zdefiniowaliśmy metody mapującej to test wyrzuci nam błąd. Aby to zweryfikować to przemianujmy ProvinceDto na VoivodeshipDto. W tym momencie po uruchomieniu testu dostaniemy komunikat (java.lang.NoClassDefFoundError) o nieznalezionej klasie pl/devcezz/mapstruct/tips/ProvinceDto. Spróbujmy to naprawić usuwając katalog target i odpalmy ponownie test. W tym momencie MapStruct wygeneruje poprawnie klasę mapującą i dostaniemy zielony pasek w swoim środowisku programistycznym.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package pl.devcezz.mapstruct.tips;

import java.util.ArrayList;
import java.util.List;
import javax.annotation.processing.Generated;

@Generated(
  value = "org.mapstruct.ap.MappingProcessor",
  date = "2022-03-19T10:36:41+0100",
  comments = "version: 1.4.2.Final, compiler: javac, environment: Java 17 (Oracle Corporation)"
)
public class CountryMapperImpl implements CountryMapper {

  //... other methods

  protected VoivodeshipDto provinceToVoivodeshipDto(Province province) {
    if ( province == null ) {
      return null;
    }

    List<CityDto> cities = null;
    String name = null;

    cities = cityListToCityDtoList( province.getCities() );
    name = province.getName();

    VoivodeshipDto voivodeshipDto = new VoivodeshipDto( name, cities );

    return voivodeshipDto;
  }

  protected List<VoivodeshipDto> provinceListToVoivodeshipDtoList(List<Province> list) {
    if ( list == null ) {
      return null;
    }

    List<VoivodeshipDto> list1 = new ArrayList<VoivodeshipDto>( list.size() );
    for ( Province province : list ) {
      list1.add( provinceToVoivodeshipDto( province ) );
    }

    return list1;
  }
}

Osobiście uważam, że nie warto mapować klas, których nazwy tak drastycznie różnią się od siebie oraz mają sporo agregowanych struktur danych. Wprowadza to duży chaos do kodu i osoby czytające go później będą musiały poświęcić więcej czasu, aby dojść do tego co dokładnie na co się mapuje.

Wykorzystanie Optional

Korzystanie z Optional pomaga nam pozbyć się problematycznego wyjątku NullPointerException. Natomiast czasami może powstać potrzeba, aby jednak nie opakowywać w ten sposób wybranego pola klasy. Z jakiegoś powodu chcielibyśmy, aby mogło ono przyjąć wartość null. Załóżmy, więc że mamy wymaganie, aby przekonwertować klasę z polem Optional na klasę bez takiego wspomagania.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package pl.devcezz.mapstruct.tips.optional;

import java.util.Optional;

public class Box {

  private Optional<String> content;

  public Box(String content) {
    this.content = Optional.ofNullable(content);
  }

  public Optional<String> getContent() {
    return content;
  }

  public void setContent(String content) {
    this.content = Optional.ofNullable(content);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
package pl.devcezz.mapstruct.tips.optional;

public class BoxDto {

  private String content;

  public BoxDto(String content) {
    this.content = content;
  }

  //... getters & setters
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package pl.devcezz.mapstruct.tips.optional;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface BoxMapper {

  BoxMapper INSTANCE = Mappers.getMapper(BoxMapper.class);

  BoxDto map(Box box);

  Box map(BoxDto dto);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package pl.devcezz.mapstruct.tips.optional;

import org.junit.jupiter.api.Test;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

public class BoxMapperTest {

  @Test
  void should_map_box_to_box_dto() {
    Box box = new Box("books");

    BoxDto result = BoxMapper.INSTANCE.map(box);

    assertThat(result.getContent()).isEqualTo("books");
  }

  @Test
  void should_map_box_dto_to_box() {
    BoxDto dto = new BoxDto("documents");

    Box result = BoxMapper.INSTANCE.map(dto);

    assertThat(result.getContent()).isEqualTo(Optional.of("documents"));
  }
}

Jeśli teraz spróbujemy uruchomić powyższy test to MapStruct niestety nam nie pozwoli na jego wykonanie. Dostaniemy w zamian następującą wskazówkę: “java: Can’t map property "Optional<String> content” to “String content”. Consider to declare/implement a mapping method: "String map(Optional<String> value)”.”. Wychodzi na to, że musimy ręcznie dodać metodę mapującą daną wartość na Optional. Aby dokładnie wskazać jak ma odbyć się konwersja należy wykorzystać domyślną implementację metody w interfejsie BoxMapper.

1
2
3
4
5
6
7
default Optional<String> map(String value) {
  return Optional.ofNullable(value);
}

default String map(Optional<String> value) {
  return value.orElse(null);
}

Po napisaniu tego kawałka kodu test się uruchamia i przechodzi na zielono. Robota wykonana, ale co w przypadku, gdyby doszły kolejne typy wymagające mapowania na Optional i odwrotnie? Musielibyśmy dodawać kolejne specjalizowane metody? Na szczęście nie. Trzeba po prostu zmienić naszą implementację, aby wykorzystywała metody generyczne.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package pl.devcezz.mapstruct.tips.optional;

import java.util.Optional;

public interface OptionalGenericMapper {

  default <T> Optional<T> map(T value) {
    return Optional.ofNullable(value);
  }

  default <T> T map(Optional<T> value) {
    return value.orElse(null);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package pl.devcezz.mapstruct.tips.optional;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface BoxMapper extends OptionalGenericMapper {

  BoxMapper INSTANCE = Mappers.getMapper(BoxMapper.class);

  BoxDto map(Box box);

  Box map(BoxDto dto);
}

Z mappera BoxMapper usuwamy nowopowstałe metody i ustawiamy, aby rozszerzał on nasz nowy interfejs. Robimy następnie sprawdzenie za pomocą testu czy wszystko działa. Jak najbardziej kod jest prawidłowy. Nie musimy się już przejmować tym, że zaskoczy nas jakiś nowy typ pola opakowanego w Optional.

Mapper w mapperze

Czasami mamy już gotowy mapper dla jednej z wcześniej zdefiniowanych, rozbudowanych struktur. W pewnym momencie przychodzi wymaganie, aby dodać i zmapować nową klasę zawierającą w sobie właśnie tą strukturę. Zamiast przenosić wszystko do jednego interfejsu, możemy stworzyć kolejny i wykorzystać w nim uprzednio utworzony mapper. Użyjemy do tego właściwość uses z adnotacji @Mapper.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package pl.devcezz.mapstruct.tips.agregation;

import java.util.Objects;

public class Person {

  private String firstname;
  private String surname;
  private Integer age;
  //... more fields

  public Person(String firstname, String surname, Integer age) {
    this.firstname = firstname;
    this.surname = surname;
    this.age = age;
  }

  //... getters & setter, equals & hashCode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package pl.devcezz.mapstruct.tips.agregation;

import java.util.Objects;

public class PersonDto {

  private String fullName;
  private Integer yearOfBirth;
  //... more fields

  public PersonDto(String fullName, Integer yearOfBirth) {
    this.fullName = fullName;
    this.yearOfBirth = yearOfBirth;
  }

  //... getters & setter, equals & hashCode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package pl.devcezz.mapstruct.tips.agregation;

import org.mapstruct.DecoratedWith;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
@DecoratedWith(PersonDecoratorMapper.class)
public interface PersonMapper {

  public PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

  PersonDto map(Person person);

  Person map(PersonDto dto);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package pl.devcezz.mapstruct.tips.agregation;

import java.time.Year;

public abstract class PersonDecoratorMapper implements PersonMapper {

  private final PersonMapper delegate;

  public PersonDecoratorMapper(PersonMapper delegate) {
    this.delegate = delegate;
  }

  @Override
  public PersonDto map(Person person) {
    PersonDto dto = delegate.map(person);
    dto.setFullName(person.getFirstname() + " " + person.getSurname());
    dto.setYearOfBirth(Year.now().getValue() - person.getAge());
    return dto;
  }

  @Override
  public Person map(PersonDto dto) {
    Person person = delegate.map(dto);
    String[] names = dto.getFullName().split(" ");
    person.setFirstname(names[0]);
    person.setSurname(names[1]);
    person.setAge(Year.now().getValue() - dto.getYearOfBirth());
    return person;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package pl.devcezz.mapstruct.tips.aggregation;

import org.junit.jupiter.api.Test;
import pl.devcezz.mapstruct.tips.agregation.Person;
import pl.devcezz.mapstruct.tips.agregation.PersonDto;
import pl.devcezz.mapstruct.tips.agregation.PersonMapper;

import static org.assertj.core.api.Assertions.assertThat;

public class PersonMapperTest {

  @Test
  void should_map_person_to_person_dto() {
    Person person = new Person("Tomasz", "Izabelski", 23);

    PersonDto result = PersonMapper.INSTANCE.map(person);

    assertThat(result.getFullName()).isEqualTo("Tomasz Izabelski");
    assertThat(result.getYearOfBirth()).isEqualTo(1999);
  }

  @Test
  void should_map_person_dto_to_person() {
    PersonDto dto = new PersonDto("Karol Zamojski", 1993);

    Person result = PersonMapper.INSTANCE.map(dto);

    assertThat(result.getFirstname()).isEqualTo("Karol");
    assertThat(result.getSurname()).isEqualTo("Zamojski");
    assertThat(result.getAge()).isEqualTo(29);
  }
}

Tą skomplikowaną strukturą w naszym przypadku jest klasa Person. W powyższym kodzie ma ona tylko 3 pola, ale załóżmy, że jest ich o wiele więcej (mniejsza ilość nie zaciemnia tak problemu). Stworzyliśmy dla tej struktury mapper wraz z dekoratorem, napisaliśmy testy i wszystko działa. Teraz dla nowego wymagania dojdzie nam klasa agregująca w sobie kolekcję typu Person.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package pl.devcezz.mapstruct.tips.agregation;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Department {

  private String name;
  private List<Person> employees;

  public Department(String name, List<Person> employees) {
    this.name = name;
    this.employees = new ArrayList<>(employees);
  }

  //... getters & setters
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package pl.devcezz.mapstruct.tips.agregation;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class DepartmentDto {

  private String name;
  private List<PersonDto> employees;

  public DepartmentDto(String name, List<PersonDto> employees) {
    this.name = name;
    this.employees = new ArrayList<>(employees);
  }

  //... getters & setters
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package pl.devcezz.mapstruct.tips.agregation;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface DepartmentMapper {

  DepartmentMapper INSTANCE = Mappers.getMapper(DepartmentMapper.class);

  DepartmentDto map(Department department);

  Department map(DepartmentDto dto);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package pl.devcezz.mapstruct.tips.aggregation;

import org.junit.jupiter.api.Test;
import pl.devcezz.mapstruct.tips.agregation.Department;
import pl.devcezz.mapstruct.tips.agregation.DepartmentDto;
import pl.devcezz.mapstruct.tips.agregation.DepartmentMapper;
import pl.devcezz.mapstruct.tips.agregation.Person;
import pl.devcezz.mapstruct.tips.agregation.PersonDto;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

public class DepartmentMapperTest {

  @Test
  void should_map_department_to_department_dto() {
    Department department = new Department(
        "accounting",
        List.of(
            new Person("Marzena", "Kobylińska", 52),
            new Person("Elżbieta", "Mariańska", 24),
            new Person("Albert", "Kot", 43)
        )
    );

    DepartmentDto result = DepartmentMapper.INSTANCE.map(department);

    assertThat(result.getName()).isEqualTo("accounting");
    assertThat(result.getEmployees()).containsExactlyInAnyOrder(
        new PersonDto("Marzena Kobylińska", 1970),
        new PersonDto("Elżbieta Mariańska", 1998),
        new PersonDto("Albert Kot", 1979)
    );
  }

  @Test
  void should_map_department_dto_to_department() {
    DepartmentDto dto = new DepartmentDto(
        "marketing",
        List.of(
            new PersonDto("Paweł Niewiadomski", 1961),
            new PersonDto("Grażyna Piechota", 1977),
            new PersonDto("Marcin Niezgoda", 2000)
        )
    );

    Department result = DepartmentMapper.INSTANCE.map(dto);

    assertThat(result.getName()).isEqualTo("marketing");
    assertThat(result.getEmployees()).containsExactlyInAnyOrder(
        new Person("Paweł", "Niewiadomski", 61),
        new Person("Grażyna", "Piechota", 45),
        new Person("Marcin", "Niezgoda", 22)
    );
  }
}

Niestety te dwa przypadki testowe już nie przechodzą. Jak się okazuje DepartmentMapper nie wie w jaki sposób ma zmapować Person na PersonDto i odwrotnie. W pola reprezentujące nazwę i wiek wpisuje wartości null. Naprawa tego problemu jest naprawdę prosta. Wystarczy tylko mała zmiana, o której napisałem powyżej.

1
2
3
4
@Mapper(uses = PersonMapper.class)
public interface DepartmentMapper {
  //...
}

Teraz testy już przejdą bez problemu, ponieważ nauczyliśmy nasz mapper jak radzić sobie ze swoją zagnieżdżoną strukturą.

Mapowanie wartości enuma

Ostatnią rzeczą, na którą chciałbym zwrócić uwagę jest mapowanie wartości jednego enuma na drugiego. Może nastąpić taka sytuacja, że te same stałe będą miały inne nazwy, ale będą oznaczały to samo jak np. SUCCESS i ACCOMPLISHED. Jak zatem rozwiązać taki problem? Z pomocą przychodzą nam dwie adnotacje: @ValueMappings i @ValueMapping.

1
2
3
4
5
package pl.devcezz.mapstruct.tips.enums;

public enum Color {
  RED, BLUE, GREEN
}
1
2
3
4
5
package pl.devcezz.mapstruct.tips.enums;

public enum ColorRbg {
  _FF0000, _0000FF, _008000
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package pl.devcezz.mapstruct.tips.enums;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface ColorMapper {

  ColorMapper INSTANCE = Mappers.getMapper(ColorMapper.class);

  Color map(ColorRbg rbg);

  ColorRbg map(Color color);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package pl.devcezz.mapstruct.tips.enums;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class ColorMapperTest {

  @Test
  void map_red_to_red_rgb() {
    Color red = Color.RED;

    ColorRbg result = ColorMapper.INSTANCE.map(red);

    assertThat(result).isEqualTo(ColorRbg._FF0000);
  }

  @Test
  void map_blue_rgb_to_red() {
    ColorRbg blue = ColorRbg._0000FF;

    Color result = ColorMapper.INSTANCE.map(blue);

    assertThat(result).isEqualTo(Color.BLUE);
  }
}

W tym momencie jeśli nie nauczymy naszego mappera jak ma dokonywać konwersji wartości to otrzymamy następujące komunikaty: “java: The following constants from the source enum have no corresponding constant in the target enum and must be be mapped via adding additional mappings: _FF0000, _0000FF, _008000, _FF9900.” oraz “java: The following constants from the source enum have no corresponding constant in the target enum and must be be mapped via adding additional mappings: RED, BLUE, GREEN.”. Aby je zlikwidować spróbujmy wykorzystać nowopoznane adnotacje.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package pl.devcezz.mapstruct.tips.enums;

import org.mapstruct.Mapper;
import org.mapstruct.ValueMapping;
import org.mapstruct.ValueMappings;
import org.mapstruct.factory.Mappers;

@Mapper
public interface ColorMapper {

  ColorMapper INSTANCE = Mappers.getMapper(ColorMapper.class);

  @ValueMappings({
      @ValueMapping(source = "_FF0000", target = "RED"),
      @ValueMapping(source = "_0000FF", target = "BLUE"),
      @ValueMapping(source = "_008000", target = "GREEN")
  })
  Color map(ColorRbg rbg);

  @ValueMappings({
      @ValueMapping(source = "RED", target = "_FF0000"),
      @ValueMapping(source = "BLUE", target = "_0000FF"),
      @ValueMapping(source = "GREEN", target = "_008000")
  })
  ColorRbg map(Color color);
}

Dwa z naszych przypadków testowych przeszły. Natomiast co w przypadku, gdy jeden z typów wyliczeniowych ma więcej wartości niż drugi? Jak wtedy zachowa się MapStruct? Przed uruchomieniem się testu dostaniemy do razu komunikat, że nie wiadomo jak ma zostać przekonwertowana nowa wartość: “java: The following constants from the source enum have no corresponding constant in the target enum and must be be mapped via adding additional mappings: _FF9900.” Tutaj z pomocą przyjdą nam stałe zdefiniowane w MappingConstants. Możemy wykorzystać wartość ANY_REMAINING, aby przemapować pozostałe wartości na np. jakąś wybraną wartość z danego enuma lub na null.

1
2
3
public enum Color {
  RED, BLUE, GREEN, UNKNOWN
}
1
2
3
public enum ColorRbg {
  _FF0000, _0000FF, _008000, _FF9900
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package pl.devcezz.mapstruct.tips.enums;

import org.mapstruct.Mapper;
import org.mapstruct.MappingConstants;
import org.mapstruct.ValueMapping;
import org.mapstruct.ValueMappings;
import org.mapstruct.factory.Mappers;

@Mapper
public interface ColorMapper {

  ColorMapper INSTANCE = Mappers.getMapper(ColorMapper.class);

  @ValueMappings({
      @ValueMapping(source = "_FF0000", target = "RED"),
      @ValueMapping(source = "_0000FF", target = "BLUE"),
      @ValueMapping(source = "_008000", target = "GREEN"),
      @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "UNKNOWN")
  })
  Color map(ColorRbg rbg);

  @ValueMappings({
      @ValueMapping(source = "RED", target = "_FF0000"),
      @ValueMapping(source = "BLUE", target = "_0000FF"),
      @ValueMapping(source = "GREEN", target = "_008000"),
      @ValueMapping(source = "UNKNOWN", target = MappingConstants.NULL)
  })
  ColorRbg map(Color color);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package pl.devcezz.mapstruct.tips.enums;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class ColorMapperTest {

  //...

  @Test
  void map_orange_rgb_to_unknown_color() {
    ColorRbg blue = ColorRbg._FF9900;

    Color result = ColorMapper.INSTANCE.map(blue);

    assertThat(result).isEqualTo(Color.UNKNOWN);
  }

  @Test
  void map_unknown_color_to_null() {
    Color unknown = Color.UNKNOWN;

    ColorRbg result = ColorMapper.INSTANCE.map(unknown);

    assertThat(result).isNull();
  }
}

Podsumowanie

To by było na tyle tych dodatkowych, nieskomplikowanych przypadków użycia dla MapStruct. Wydaje mi się, że udało mi się zawszeć wszystkie informacje jakie chciałem przekazać na temat tej biblioteki w tych kilku artykułach. Sam nie byłem zwolennikiem tego rozwiązania, ale z czasem się do niego przekonałem. Głównie wpłynął na to fakt, że znacznie szybciej można dostarczyć działający kod mapujący dane. Zamiast samemu ręcznie tworzyć coś od nowa można skorzystać z gotowego, przetestowanego rozwiązania. Zachęcam Cię do spróbowania tych kilka prostych instrukcji w swoim kodzie i przekonania się o możliwościach MapStruct.