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