diff --git a/libraries-6/pom.xml b/libraries-6/pom.xml
index 4b33df9a1d..cecc9ca476 100644
--- a/libraries-6/pom.xml
+++ b/libraries-6/pom.xml
@@ -106,6 +106,11 @@
libphonenumber
${libphonenumber.version}
+
+ org.modelmapper
+ modelmapper
+ ${org.modelmapper.version}
+
@@ -149,6 +154,7 @@
1.8.1
4.4
8.12.9
+ 2.4.4
-
\ No newline at end of file
+
diff --git a/libraries-6/src/main/java/com/baeldung/modelmapper/domain/Game.java b/libraries-6/src/main/java/com/baeldung/modelmapper/domain/Game.java
new file mode 100644
index 0000000000..f59cb7cec4
--- /dev/null
+++ b/libraries-6/src/main/java/com/baeldung/modelmapper/domain/Game.java
@@ -0,0 +1,74 @@
+package com.baeldung.modelmapper.domain;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class Game {
+
+ private Long id;
+ private String name;
+ private Long timestamp;
+
+ private Player creator;
+ private final List players = new ArrayList<>();
+
+ private GameSettings settings;
+
+ public Game() {
+ }
+
+ public Game(Long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public Long getId() {
+ return this.id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Long getTimestamp() {
+ return this.timestamp;
+ }
+
+ public void setTimestamp(Long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public Player getCreator() {
+ return this.creator;
+ }
+
+ public void setCreator(Player creator) {
+ this.creator = creator;
+ addPlayer(creator);
+ }
+
+ public void addPlayer(Player player) {
+ this.players.add(player);
+ }
+
+ public List getPlayers() {
+ return Collections.unmodifiableList(this.players);
+ }
+
+ public GameSettings getSettings() {
+ return this.settings;
+ }
+
+ public void setSettings(GameSettings settings) {
+ this.settings = settings;
+ }
+}
diff --git a/libraries-6/src/main/java/com/baeldung/modelmapper/domain/GameMode.java b/libraries-6/src/main/java/com/baeldung/modelmapper/domain/GameMode.java
new file mode 100644
index 0000000000..41e16d9891
--- /dev/null
+++ b/libraries-6/src/main/java/com/baeldung/modelmapper/domain/GameMode.java
@@ -0,0 +1,5 @@
+package com.baeldung.modelmapper.domain;
+
+public enum GameMode {
+ NORMAL, TURBO
+}
diff --git a/libraries-6/src/main/java/com/baeldung/modelmapper/domain/GameSettings.java b/libraries-6/src/main/java/com/baeldung/modelmapper/domain/GameSettings.java
new file mode 100644
index 0000000000..81ba785ba4
--- /dev/null
+++ b/libraries-6/src/main/java/com/baeldung/modelmapper/domain/GameSettings.java
@@ -0,0 +1,31 @@
+package com.baeldung.modelmapper.domain;
+
+public class GameSettings {
+
+ private GameMode mode;
+ private int maxPlayers;
+
+ public GameSettings() {
+ }
+
+ public GameSettings(GameMode mode, int maxPlayers) {
+ this.mode = mode;
+ this.maxPlayers = maxPlayers;
+ }
+
+ public GameMode getMode() {
+ return this.mode;
+ }
+
+ public void setMode(GameMode mode) {
+ this.mode = mode;
+ }
+
+ public int getMaxPlayers() {
+ return this.maxPlayers;
+ }
+
+ public void setMaxPlayers(int maxPlayers) {
+ this.maxPlayers = maxPlayers;
+ }
+}
diff --git a/libraries-6/src/main/java/com/baeldung/modelmapper/domain/Player.java b/libraries-6/src/main/java/com/baeldung/modelmapper/domain/Player.java
new file mode 100644
index 0000000000..f386e72a19
--- /dev/null
+++ b/libraries-6/src/main/java/com/baeldung/modelmapper/domain/Player.java
@@ -0,0 +1,41 @@
+package com.baeldung.modelmapper.domain;
+
+public class Player {
+
+ private Long id;
+ private String name;
+
+ private Game currentGame;
+
+ public Player() {
+ }
+
+ public Player(Long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public Long getId() {
+ return this.id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Game getCurrentGame() {
+ return this.currentGame;
+ }
+
+ public void setCurrentGame(Game currentGame) {
+ this.currentGame = currentGame;
+ }
+}
diff --git a/libraries-6/src/main/java/com/baeldung/modelmapper/dto/GameDTO.java b/libraries-6/src/main/java/com/baeldung/modelmapper/dto/GameDTO.java
new file mode 100644
index 0000000000..1c8111809c
--- /dev/null
+++ b/libraries-6/src/main/java/com/baeldung/modelmapper/dto/GameDTO.java
@@ -0,0 +1,92 @@
+package com.baeldung.modelmapper.dto;
+
+import com.baeldung.modelmapper.domain.GameMode;
+import java.util.List;
+
+public class GameDTO {
+
+ private Long id;
+ private String name;
+ private Long creationTime;
+
+ private String creatorId;
+ private String creator;
+
+ private int totalPlayers;
+ private List players;
+
+ private GameMode mode;
+ private int maxPlayers;
+
+ public Long getId() {
+ return this.id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Long getCreationTime() {
+ return this.creationTime;
+ }
+
+ public void setCreationTime(Long creationTime) {
+ this.creationTime = creationTime;
+ }
+
+ public String getCreatorId() {
+ return this.creatorId;
+ }
+
+ public void setCreatorId(String creatorId) {
+ this.creatorId = creatorId;
+ }
+
+ public String getCreator() {
+ return this.creator;
+ }
+
+ public void setCreator(String creator) {
+ this.creator = creator;
+ }
+
+ public int getTotalPlayers() {
+ return this.totalPlayers;
+ }
+
+ public void setTotalPlayers(int totalPlayers) {
+ this.totalPlayers = totalPlayers;
+ }
+
+ public GameMode getMode() {
+ return this.mode;
+ }
+
+ public void setMode(GameMode mode) {
+ this.mode = mode;
+ }
+
+ public int getMaxPlayers() {
+ return this.maxPlayers;
+ }
+
+ public void setMaxPlayers(int maxPlayers) {
+ this.maxPlayers = maxPlayers;
+ }
+
+ public List getPlayers() {
+ return this.players;
+ }
+
+ public void setPlayers(List players) {
+ this.players = players;
+ }
+}
diff --git a/libraries-6/src/main/java/com/baeldung/modelmapper/dto/PlayerDTO.java b/libraries-6/src/main/java/com/baeldung/modelmapper/dto/PlayerDTO.java
new file mode 100644
index 0000000000..c4e5a222c9
--- /dev/null
+++ b/libraries-6/src/main/java/com/baeldung/modelmapper/dto/PlayerDTO.java
@@ -0,0 +1,33 @@
+package com.baeldung.modelmapper.dto;
+
+public class PlayerDTO {
+
+ private Long id;
+ private String name;
+
+ private GameDTO currentGame;
+
+ public Long getId() {
+ return this.id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public GameDTO getCurrentGame() {
+ return this.currentGame;
+ }
+
+ public void setCurrentGame(GameDTO currentGame) {
+ this.currentGame = currentGame;
+ }
+}
diff --git a/libraries-6/src/main/java/com/baeldung/modelmapper/repository/GameRepository.java b/libraries-6/src/main/java/com/baeldung/modelmapper/repository/GameRepository.java
new file mode 100644
index 0000000000..80b861c981
--- /dev/null
+++ b/libraries-6/src/main/java/com/baeldung/modelmapper/repository/GameRepository.java
@@ -0,0 +1,28 @@
+package com.baeldung.modelmapper.repository;
+
+import com.baeldung.modelmapper.domain.Game;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Sample in-memory Game Repository
+ */
+public class GameRepository {
+
+ private final List gameStore = new ArrayList<>();
+
+ public GameRepository() {
+ // initialize some test data
+ this.gameStore.add(new Game(1L, "Game 1"));
+ this.gameStore.add(new Game(2L, "Game 2"));
+ this.gameStore.add(new Game(3L, "Game 3"));
+ }
+
+ public Game findById(Long id) {
+ return this.gameStore.stream()
+ .filter(g -> g.getId().equals(id))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("No Game found"));
+ }
+
+}
diff --git a/libraries-6/src/test/java/com/baeldung/modelmapper/ModelMapperUnitTest.java b/libraries-6/src/test/java/com/baeldung/modelmapper/ModelMapperUnitTest.java
new file mode 100644
index 0000000000..69bc4059b0
--- /dev/null
+++ b/libraries-6/src/test/java/com/baeldung/modelmapper/ModelMapperUnitTest.java
@@ -0,0 +1,270 @@
+package com.baeldung.modelmapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.baeldung.modelmapper.domain.Game;
+import com.baeldung.modelmapper.domain.GameMode;
+import com.baeldung.modelmapper.domain.GameSettings;
+import com.baeldung.modelmapper.domain.Player;
+import com.baeldung.modelmapper.dto.GameDTO;
+import com.baeldung.modelmapper.dto.PlayerDTO;
+import com.baeldung.modelmapper.repository.GameRepository;
+import java.time.Instant;
+import java.util.Collection;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.modelmapper.Condition;
+import org.modelmapper.Conditions;
+import org.modelmapper.Converter;
+import org.modelmapper.ModelMapper;
+import org.modelmapper.Provider;
+import org.modelmapper.TypeMap;
+import org.modelmapper.convention.MatchingStrategies;
+
+public class ModelMapperUnitTest {
+
+ ModelMapper mapper;
+ GameRepository gameRepository;
+
+ @BeforeEach
+ public void setup() {
+ this.mapper = new ModelMapper();
+ this.gameRepository = new GameRepository();
+ }
+
+ @Test
+ public void whenMapGameWithExactMatch_thenConvertsToDTO() {
+ // when similar source object is provided
+ Game game = new Game(1L, "Game 1");
+ GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
+
+ // then it maps without property mapper
+ assertEquals(game.getId(), gameDTO.getId());
+ assertEquals(game.getName(), gameDTO.getName());
+ }
+
+ @Test
+ public void whenMapGameWithBasicPropertyMapping_thenConvertsToDTO() {
+ // setup
+ TypeMap propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
+ propertyMapper.addMapping(Game::getTimestamp, GameDTO::setCreationTime);
+
+ // when field names are different
+ Game game = new Game(1L, "Game 1");
+ game.setTimestamp(Instant.now().getEpochSecond());
+ GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
+
+ // then it maps via property mapper
+ assertEquals(game.getId(), gameDTO.getId());
+ assertEquals(game.getName(), gameDTO.getName());
+ assertEquals(game.getTimestamp(), gameDTO.getCreationTime());
+ }
+
+ @Test
+ public void whenMapGameWithDeepMapping_thenConvertsToDTO() {
+ // setup
+ TypeMap propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
+ // add deep mapping to flatten source's Player into name in destination
+ propertyMapper.addMappings(
+ mapper -> mapper.map(src -> src.getCreator().getName(), GameDTO::setCreator)
+ );
+
+ // when map between different hierarchies
+ Game game = new Game(1L, "Game 1");
+ game.setCreator(new Player(1L, "John"));
+ GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
+
+ // then
+ assertEquals(game.getId(), gameDTO.getId());
+ assertEquals(game.getName(), gameDTO.getName());
+ assertEquals(game.getCreator().getName(), gameDTO.getCreator());
+ }
+
+ @Test
+ public void whenMapGameWithDifferentTypedProperties_thenConvertsToDTO() {
+ // setup
+ TypeMap propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
+ propertyMapper.addMappings(mapper -> mapper.map(src -> src.getCreator().getId(), GameDTO::setCreatorId));
+
+ // when map different typed properties
+ Game game = new Game(1L, "Game 1");
+ game.setCreator(new Player(1L, "John"));
+ GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
+
+ // then it converts between types
+ assertEquals(game.getId(), gameDTO.getId());
+ assertEquals(game.getName(), gameDTO.getName());
+ assertEquals("1", gameDTO.getCreatorId());
+ }
+
+ @Test
+ public void whenMapGameWithSkipIdProperty_thenConvertsToDTO() {
+ // setup
+ TypeMap propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
+ propertyMapper.addMappings(mapper -> mapper.skip(GameDTO::setId));
+
+ // when id is skipped
+ Game game = new Game(1L, "Game 1");
+ GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
+
+ // then destination id is null
+ assertNull(gameDTO.getId());
+ assertEquals(game.getName(), gameDTO.getName());
+ }
+
+ @Test
+ public void whenMapGameWithCustomConverter_thenConvertsToDTO() {
+ // setup
+ TypeMap propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
+ Converter collectionToSize = c -> c.getSource().size();
+ propertyMapper.addMappings(
+ mapper -> mapper.using(collectionToSize).map(Game::getPlayers, GameDTO::setTotalPlayers)
+ );
+
+ // when collection to size converter is provided
+ Game game = new Game(1L, "Game 1");
+ game.setCreator(new Player(1L, "John"));
+ game.addPlayer(new Player(2L, "Bob"));
+ GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
+
+ // then it maps the size to a custom field
+ assertEquals(game.getId(), gameDTO.getId());
+ assertEquals(game.getName(), gameDTO.getName());
+ assertEquals(game.getPlayers().size(), gameDTO.getTotalPlayers());
+ }
+
+ @Test
+ public void whenUsingProvider_thenMergesGameInstances() {
+ // setup
+ TypeMap propertyMapper = this.mapper.createTypeMap(Game.class, Game.class);
+ // a provider to fetch a Game instance from a repository
+ Provider gameProvider = p -> this.gameRepository.findById(1L);
+ propertyMapper.setProvider(gameProvider);
+
+ // when a state for update is given
+ Game update = new Game(1L, "Game Updated!");
+ update.setCreator(new Player(1L, "John"));
+ Game updatedGame = this.mapper.map(update, Game.class);
+
+ // then it merges the updates over on the provided instance
+ assertEquals(1L, updatedGame.getId().longValue());
+ assertEquals("Game Updated!", updatedGame.getName());
+ assertEquals("John", updatedGame.getCreator().getName());
+ }
+
+ @Test
+ public void whenUsingConditionalIsNull_thenMergesGameInstancesWithoutOverridingId() {
+ // setup
+ TypeMap propertyMapper = this.mapper.createTypeMap(Game.class, Game.class);
+ propertyMapper.setProvider(p -> this.gameRepository.findById(2L));
+ propertyMapper.addMappings(mapper -> mapper.when(Conditions.isNull()).skip(Game::getId, Game::setId));
+
+ // when game has no id
+ Game update = new Game(null, "Not Persisted Game!");
+ Game updatedGame = this.mapper.map(update, Game.class);
+
+ // then destination game id is not overwritten
+ assertEquals(2L, updatedGame.getId().longValue());
+ assertEquals("Not Persisted Game!", updatedGame.getName());
+ }
+
+ @Test
+ public void whenUsingCustomConditional_thenConvertsDTOSkipsZeroTimestamp() {
+ // setup
+ TypeMap propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
+ Condition hasTimestamp = ctx -> ctx.getSource() != null && ctx.getSource() > 0;
+ propertyMapper.addMappings(mapper -> mapper.when(hasTimestamp).map(Game::getTimestamp, GameDTO::setCreationTime));
+
+ // when game has zero timestamp
+ Game game = new Game(1L, "Game 1");
+ game.setTimestamp(0L);
+ GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
+
+ // then timestamp field is not mapped
+ assertEquals(game.getId(), gameDTO.getId());
+ assertEquals(game.getName(), gameDTO.getName());
+ assertNotEquals(0L, gameDTO.getCreationTime());
+ assertNull(gameDTO.getCreationTime());
+
+ // when game has timestamp greater than zero
+ game.setTimestamp(Instant.now().getEpochSecond());
+ gameDTO = this.mapper.map(game, GameDTO.class);
+
+ // then timestamp field is mapped
+ assertEquals(game.getId(), gameDTO.getId());
+ assertEquals(game.getName(), gameDTO.getName());
+ assertEquals(game.getTimestamp(), gameDTO.getCreationTime());
+ assertNotNull(gameDTO.getCreationTime());
+ }
+
+ @Test
+ public void whenUsingLooseMappingStrategy_thenConvertsToDomainAndDTO() {
+ // setup
+ this.mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.LOOSE);
+
+ // when dto has flat fields for GameSetting
+ GameDTO gameDTO = new GameDTO();
+ gameDTO.setId(1L);
+ gameDTO.setName("Game 1");
+ gameDTO.setMode(GameMode.TURBO);
+ gameDTO.setMaxPlayers(8);
+ Game game = this.mapper.map(gameDTO, Game.class);
+
+ // then it converts to inner objects without property mapper
+ assertEquals(gameDTO.getId(), game.getId());
+ assertEquals(gameDTO.getName(), game.getName());
+ assertEquals(gameDTO.getMode(), game.getSettings().getMode());
+ assertEquals(gameDTO.getMaxPlayers(), game.getSettings().getMaxPlayers());
+
+ // when the GameSetting's field names match
+ game = new Game(1L, "Game 1");
+ game.setSettings(new GameSettings(GameMode.NORMAL, 6));
+ gameDTO = this.mapper.map(game, GameDTO.class);
+
+ // then it flattens the fields on dto
+ assertEquals(game.getId(), gameDTO.getId());
+ assertEquals(game.getName(), gameDTO.getName());
+ assertEquals(game.getSettings().getMode(), gameDTO.getMode());
+ assertEquals(game.getSettings().getMaxPlayers(), gameDTO.getMaxPlayers());
+ }
+
+ @Test
+ public void whenConfigurationSkipNullEnabled_thenConvertsToDTO() {
+ // setup
+ this.mapper.getConfiguration().setSkipNullEnabled(true);
+ TypeMap propertyMap = this.mapper.createTypeMap(Game.class, Game.class);
+ propertyMap.setProvider(p -> this.gameRepository.findById(2L));
+
+ // when game has no id
+ Game update = new Game(null, "Not Persisted Game!");
+ Game updatedGame = this.mapper.map(update, Game.class);
+
+ // then destination game id is not overwritten
+ assertEquals(2L, updatedGame.getId().longValue());
+ assertEquals("Not Persisted Game!", updatedGame.getName());
+ }
+
+ @Test
+ public void whenConfigurationPreferNestedPropertiesDisabled_thenConvertsCircularReferencedToDTO() {
+ // setup
+ this.mapper.getConfiguration().setPreferNestedProperties(false);
+
+ // when game has circular reference: Game -> Player -> Game
+ Game game = new Game(1L, "Game 1");
+ Player player = new Player(1L, "John");
+ player.setCurrentGame(game);
+ game.setCreator(player);
+ GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
+
+ // then it resolves without any exception
+ assertEquals(game.getId(), gameDTO.getId());
+ assertEquals(game.getName(), gameDTO.getName());
+ PlayerDTO playerDTO = gameDTO.getPlayers().get(0);
+ assertEquals(player.getId(), playerDTO.getId());
+ assertEquals(player.getCurrentGame().getId(), playerDTO.getCurrentGame().getId());
+ }
+
+}