diff --git a/src/main/java/hdm/mi/growbros/util/EntityMapper.java b/src/main/java/hdm/mi/growbros/util/EntityMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..333a09e7ae2e6cd6b63c170f72c21aa192dec4c8 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/util/EntityMapper.java @@ -0,0 +1,96 @@ +package hdm.mi.growbros.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +@Component +public class EntityMapper { + private final static Logger log = LoggerFactory.getLogger(EntityMapper.class); + + /** + * Updates fields of an existing entity based on non-null values from an update request. + * This method is designed for use in PATCH operations within a Spring Boot application. + * + * <p> + * The {@code map} method facilitates the partial update of an existing entity by mapping non-null + * fields from an update request object to the corresponding fields in the existing entity. + * Fields in the update request with null values are excluded from the update. + * </p> + * + * <p> + * Usage example: + * <pre>{@code + * // Create an instance of the class containing the map method + * EntityMapper entityMapper = new EntityMapper(); + * + * // Create an instance of the existing entity and update request + * ExistingEntity existingEntity = // ... (initialize existing entity) + * UpdateRequest updateRequest = // ... (initialize update request) + * + * // Perform the partial update + * entityMapper.map(updateRequest, existingEntity); + * }</pre> + * </p> + * + * <p> + * Note: This class assumes proper usage in a Spring Boot application, particularly for PATCH + * operations, where only specified fields need to be updated without affecting the entire entity. + * </p> + * + * @param updateRequest The object containing fields with non-null values to be used for updating. + * @param existingEntity The entity object to be updated with non-null values from the update request. + */ + public void map(Object updateRequest, Object existingEntity) { + if (updateRequest == null) { + log.warn("Update request was null, no mapping will take place"); + return; + } + if (existingEntity == null) { + log.warn("Existing entity was null, no mapping will take place"); + return; + } + + final Field[] allUpdaterFields = updateRequest.getClass().getDeclaredFields(); + final List<Field> toUpdate = new ArrayList<>(); + + for (Field updaterField : allUpdaterFields) { + updaterField.setAccessible(true); + try { + if (updaterField.get(updateRequest) != null && !"this$0".equals(updaterField.getName())) { + toUpdate.add(updaterField); + } + } catch (IllegalAccessException e) { + log.error("Illegal access exception while mapping object", e); + } + } + + updateFields(toUpdate, updateRequest, existingEntity); + } + + private Field getFieldForName(String name, Object o) { + var fields = o.getClass().getDeclaredFields(); + for (Field f : fields) { + if (name.equals(f.getName())) return f; + } + throw new IllegalArgumentException("No field with declared name '" + name + "' is present on this object"); + } + + private void updateFields(List<Field> toUpdate, Object updateRequest, Object existingEntity) { + for (Field updateField : toUpdate) { + try { + updateField.setAccessible(true); + Field entityField = getFieldForName(updateField.getName(), existingEntity); + entityField.setAccessible(true); + final Object newValue = updateField.get(updateRequest); + entityField.set(existingEntity, newValue); + } catch (IllegalAccessException e) { + log.error("Illegal access exception while mapping object", e); + } + } + } +} diff --git a/src/test/java/hdm/mi/growbros/util/EntityMapperTest.java b/src/test/java/hdm/mi/growbros/util/EntityMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6a344778103ead98145758bb293d085b8b879ea6 --- /dev/null +++ b/src/test/java/hdm/mi/growbros/util/EntityMapperTest.java @@ -0,0 +1,67 @@ +package hdm.mi.growbros.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class EntityMapperTest { + private EntityMapper entityMapper; + + @BeforeEach + void setup() { + entityMapper = new EntityMapper(); + } + + @Test + void shouldIgnore_whenEitherArgument_isNull() { + //arrange + final Entity entity = getBaseEntity(); + final UpdaterObject updater = new UpdaterObject(); + + //act and assert + assertAll( + () -> assertDoesNotThrow(() -> entityMapper.map(null, entity)), + () -> assertDoesNotThrow(() -> entityMapper.map(updater, null)) + ); + } + + @Test + void shouldUpdate_toNewValues() { + //arrange + final Entity entity = getBaseEntity(); + + final UpdaterObject updater = new UpdaterObject(); + final String expected = "UPDATED value 1"; + updater.value1 = expected; + + //act + entityMapper.map(updater, entity); + + //assert + assertAll( + () -> assertEquals(1, entity.id), + () -> assertEquals(expected, entity.value1), + () -> assertEquals("Value 2", entity.value2) + ); + } + + private class UpdaterObject { + private String value1; + private String value2; + } + + private class Entity { + private int id; + private String value1; + private String value2; + } + + private Entity getBaseEntity() { + final Entity entity = new Entity(); + entity.id = 1; + entity.value1 = "Value 1"; + entity.value2 = "Value 2"; + return entity; + } +}